mirror of
https://github.com/zjs81/meshcore-open.git
synced 2026-06-15 07:04:26 +10:00
Compare commits
371 Commits
Alpha4
...
feature/usb
| Author | SHA1 | Date | |
|---|---|---|---|
| fef73b7b62 | |||
| 8238b6197f | |||
| 435ba89982 | |||
| 0565cee461 | |||
| ab2b509d6a | |||
| eba95af31f | |||
| 04c016cfe1 | |||
| ea2354712d | |||
| 7a0b8aad3d | |||
| bd34bb5e88 | |||
| 81548fdc21 | |||
| b2770ef028 | |||
| 7c479f9121 | |||
| 1f2dfc555b | |||
| 8eb6f32fef | |||
| d96cd34771 | |||
| fb58a3262c | |||
| f584c4fba0 | |||
| b5b930646f | |||
| 3452bdae8c | |||
| 25fc9454a8 | |||
| 524558c511 | |||
| 367e47bb1e | |||
| 21ff765e41 | |||
| 38d40ca0a4 | |||
| 5b4535d5dc | |||
| f9b6299620 | |||
| 7cb84dbf6f | |||
| 44c0670dae | |||
| 74da9e82b5 | |||
| 63583dadda | |||
| 32632669c3 | |||
| 3c0c0d1dea | |||
| e6c9a3fea7 | |||
| f5154b0033 | |||
| 4c7ee3b3b0 | |||
| c2f544eeba | |||
| 98cdac4309 | |||
| d6d11eaad2 | |||
| 3cef9e81b6 | |||
| 5216e00807 | |||
| a0feb129e1 | |||
| f39a22668e | |||
| 781090243c | |||
| ca5784f3f8 | |||
| dcad5c586d | |||
| 4b24506310 | |||
| 47c4e0fb82 | |||
| c041e05972 | |||
| 612612795a | |||
| 3cec3dc233 | |||
| 3542adad1d | |||
| 115689ad95 | |||
| 9a0572e8e4 | |||
| 2d1160d992 | |||
| ee3af52c0f | |||
| 98f7c3b088 | |||
| f462815775 | |||
| 5f4333398e | |||
| c23a1da430 | |||
| 22a53439b1 | |||
| 7d8e049745 | |||
| 3502559fae | |||
| e125318137 | |||
| d53465d13b | |||
| a0efbbe4bd | |||
| bd5db9a9d5 | |||
| 79b17b53a0 | |||
| 647fe1523e | |||
| b7d5ee5754 | |||
| 38856c67e5 | |||
| 6bd3c17cdf | |||
| 6d0712c450 | |||
| ddeb1edc2e | |||
| 8d73602509 | |||
| fcab69f9f0 | |||
| d2640e1294 | |||
| b02225c02e | |||
| 128e99e3e7 | |||
| 12bf46bba1 | |||
| 92d8e7cd0b | |||
| 75610695c2 | |||
| 57ea30cae9 | |||
| e139383335 | |||
| 64428294c9 | |||
| e7a8c36bc4 | |||
| 2a62390903 | |||
| 75d25f6312 | |||
| 2a3119544c | |||
| fb41a5bf10 | |||
| d88786bb0f | |||
| e3148dd449 | |||
| 96371c03ae | |||
| cac65face6 | |||
| bdb1eb6b42 | |||
| f2ccec2926 | |||
| 31671958d5 | |||
| ea379ce50b | |||
| 50af2e0bc9 | |||
| d5ac84430c | |||
| 190fd3b353 | |||
| a2d1cb2a99 | |||
| 83386a8cde | |||
| acc0fff2dc | |||
| a26055c93f | |||
| 5a70ed48cf | |||
| a777236cd9 | |||
| a42cf77a70 | |||
| 31db565ebf | |||
| 515b9c1f29 | |||
| ea1d728d4f | |||
| 86bde1d178 | |||
| de63733bb9 | |||
| c880c2d107 | |||
| 2a7cc28a3a | |||
| 8a16024642 | |||
| 0f17e2382c | |||
| 6065059241 | |||
| faefef14ff | |||
| ddc87f3a27 | |||
| 2188b49726 | |||
| 1a9b7b0d55 | |||
| 74e29a6c0f | |||
| 7740698cde | |||
| 972ae809e3 | |||
| deb46553f3 | |||
| 58fc55df13 | |||
| ea2f35ec2e | |||
| e2585c0992 | |||
| 78f1a7b28e | |||
| 0121b5f649 | |||
| ec14870aed | |||
| c0516a475d | |||
| b998186430 | |||
| 16b2c24983 | |||
| c8ff0cc943 | |||
| 64bf307d09 | |||
| 88f8066ed3 | |||
| c8f93f9902 | |||
| c34be44950 | |||
| bf5fadd15e | |||
| 3730b2a6c2 | |||
| 173fdf7168 | |||
| 549fc62632 | |||
| 53d073d8f2 | |||
| 7465e81996 | |||
| 677b25908a | |||
| fc55fb98ce | |||
| 2bdd9d35cc | |||
| 1f816f7e08 | |||
| bd27c90216 | |||
| 9bcb8b9ca6 | |||
| aaf79c90c9 | |||
| 08edd2696e | |||
| 0f2d18d6fa | |||
| 298951f8bc | |||
| f3db63ceea | |||
| 47044ae14e | |||
| f4dd76a459 | |||
| ab76a52d71 | |||
| 332bb5ef3a | |||
| 81a423d096 | |||
| 700e85b13d | |||
| 9a27953a6e | |||
| abde4a5e46 | |||
| 6e1cb0482f | |||
| c28b38a233 | |||
| 722caf774e | |||
| 4975b5366e | |||
| d269e181c3 | |||
| 35498c1b90 | |||
| bf4f52a4e3 | |||
| c284e571b0 | |||
| a1ee0789a6 | |||
| 3ca53e967c | |||
| 096e0a4184 | |||
| 40ac95e8e6 | |||
| 377f1df445 | |||
| 9865a03c53 | |||
| a5555bd606 | |||
| 1b4d31a36e | |||
| 8e07440114 | |||
| 71129bdf4d | |||
| ab05cf8b3e | |||
| 452e5337f0 | |||
| 6ac987e7cf | |||
| 5522f9a236 | |||
| b4f79c1aae | |||
| b08defcff4 | |||
| 5676cbd84e | |||
| cf8f01128b | |||
| b5e47ce44f | |||
| 7b2f75047c | |||
| 6d63e49938 | |||
| c7b33f1d1b | |||
| 7288f11c88 | |||
| 2306269384 | |||
| 41ff2353a4 | |||
| b3ad54f296 | |||
| 7cb4c5a334 | |||
| bb8ad70cb9 | |||
| 8fe4129204 | |||
| 2feff809ff | |||
| 51d70ce086 | |||
| b05b62eeee | |||
| 061b715694 | |||
| f38b8b0319 | |||
| 304c389669 | |||
| 7acfe47fd7 | |||
| f4b18d97a1 | |||
| d2b693e5ce | |||
| ba2763a3f6 | |||
| 0c4910e149 | |||
| 4bf2519559 | |||
| 19edeab9d5 | |||
| 0e81d75cce | |||
| 9437846127 | |||
| 50ab46ed40 | |||
| dc193be8ed | |||
| 8a804a3706 | |||
| 1dc90d0e89 | |||
| 5f2312e086 | |||
| 4239fb11ed | |||
| 5fae2e5f73 | |||
| 947fafbbb7 | |||
| a9fbf8c7f5 | |||
| 72f0aa7208 | |||
| f87d4896ab | |||
| 9250dfec31 | |||
| 37db955ab2 | |||
| 739d9475c0 | |||
| b526175be4 | |||
| 73081862ad | |||
| fac062a100 | |||
| ef6bd78632 | |||
| 01c8390989 | |||
| c05f813d65 | |||
| c52b19b09f | |||
| 6a666839b6 | |||
| bc77f7e287 | |||
| 9332d8126f | |||
| 9ce00556ec | |||
| 4995f5f380 | |||
| 4e6e7b6061 | |||
| aa350aa4ae | |||
| dfd38b19e9 | |||
| 4afab3f629 | |||
| 67816130ac | |||
| d573f0c312 | |||
| 5b699cd624 | |||
| a4d3d248a5 | |||
| 2a3f2b3a24 | |||
| 675083fa01 | |||
| 5fc4b80b16 | |||
| 84a32c1e67 | |||
| 607583060a | |||
| 71cf556b61 | |||
| c26174ad18 | |||
| 04021a39a1 | |||
| fe23e9f7a0 | |||
| d7ec1876af | |||
| 87a2807f5b | |||
| daca42701c | |||
| ea43cf17eb | |||
| 8ef6e2c656 | |||
| 24de98d5ee | |||
| 0fd841b5b5 | |||
| c365b7889b | |||
| 2db30ace6a | |||
| 0d8801fa75 | |||
| bcae6ac19f | |||
| 2f4b230b31 | |||
| 98e0b05e73 | |||
| 2a909e6081 | |||
| d1009d3c20 | |||
| 91b1696bc5 | |||
| 978ea4790d | |||
| 8b1228bf8d | |||
| ddee76ced2 | |||
| 6a3c59fa2c | |||
| a54cc78691 | |||
| 05fb5a13fa | |||
| c320378be1 | |||
| b3645481c7 | |||
| 589707aa13 | |||
| 6070802213 | |||
| 2525b9425b | |||
| b786c90514 | |||
| a35590a407 | |||
| 8d15f7cef6 | |||
| e449f5e1d5 | |||
| b34d684e67 | |||
| 488a286701 | |||
| c742d98fbb | |||
| 1d4c9ad9bd | |||
| 818f514702 | |||
| be54419e5b | |||
| 00eb1a68a6 | |||
| 79ffc21bd6 | |||
| 0374f4f5da | |||
| 4650584f9b | |||
| 8d8b938878 | |||
| e3a0bd3b13 | |||
| 4f83d87f8c | |||
| 6d7d51f0a4 | |||
| 33680f0cb9 | |||
| 5115d8bbe3 | |||
| d30e7c4e2c | |||
| 8470171e88 | |||
| ede3142d40 | |||
| 6712088fcd | |||
| 7b519854d7 | |||
| 90ce46392a | |||
| d61ec217fc | |||
| 3ac81a5448 | |||
| 7004067839 | |||
| 935b7b07eb | |||
| cdacc54421 | |||
| bf8f002d55 | |||
| 998ff50495 | |||
| 92d2b224e7 | |||
| 34a6b5d895 | |||
| c953a1a798 | |||
| 42115bf200 | |||
| d0c8fab6fb | |||
| eeb8ff34e8 | |||
| 641307a316 | |||
| c37abb63e3 | |||
| 898ef1c11c | |||
| 749f9d4dfd | |||
| 9c1b5899fb | |||
| cacb9bc677 | |||
| 0ebd688787 | |||
| bb18038f60 | |||
| fcf741b20a | |||
| 88aa104ae5 | |||
| 90f90ad7cf | |||
| 8b0bdd9a46 | |||
| 45d914de57 | |||
| 2c49534955 | |||
| c56cf9c3ed | |||
| fee4cd13be | |||
| a53d5ccfb6 | |||
| e5d06b1c7e | |||
| e95a55e4f0 | |||
| 422ca941c2 | |||
| 3098d860e9 | |||
| f0d34f7503 | |||
| daa0c3f9c3 | |||
| 09e1cd2b8d | |||
| fa514533eb | |||
| 75b8b8af70 | |||
| 115667a27c | |||
| cfb51d96ff | |||
| 75356fe20d | |||
| 2089613696 | |||
| c43df67fac | |||
| e2b9b58d7d | |||
| d6794bc8d7 | |||
| 72216e2cf7 | |||
| 2a2275ec31 | |||
| dff037535d | |||
| 297e609b3e | |||
| 20171c491f | |||
| cc43f4d198 | |||
| 537384ea5b | |||
| a0be63b2e7 | |||
| 1cc887e5bb | |||
| 26d9029538 | |||
| 30bcbedf5e | |||
| 4003519deb |
@@ -2,6 +2,8 @@ name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
|
||||
@@ -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
|
||||
@@ -1,8 +1,10 @@
|
||||
name: Flutter Analyze
|
||||
name: Flutter and Dart
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
@@ -19,5 +21,11 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Analyze
|
||||
- name: Analyze code
|
||||
run: flutter analyze --fatal-infos --fatal-warnings
|
||||
|
||||
- name: Verify formatting
|
||||
run: dart format --output=none --set-exit-if-changed .
|
||||
|
||||
- name: Run tests
|
||||
run: flutter test -r github
|
||||
@@ -30,6 +30,7 @@ migrate_working_dir/
|
||||
.flutter-plugins-dependencies
|
||||
.pub-cache/
|
||||
.pub/
|
||||
pubspec.lock
|
||||
/build/
|
||||
/coverage/
|
||||
|
||||
@@ -65,6 +66,7 @@ secrets.dart
|
||||
**/ios/Flutter/Flutter.podspec
|
||||
|
||||
# Android
|
||||
.gradle/
|
||||
**/android/.gradle/
|
||||
**/android/captures/
|
||||
**/android/local.properties
|
||||
@@ -81,3 +83,6 @@ keystore.properties
|
||||
# IDE
|
||||
.vscode/launch.json
|
||||
.vscode/settings.json
|
||||
|
||||
# Cloudflare Wrangler
|
||||
.wrangler
|
||||
@@ -0,0 +1 @@
|
||||
4.0.0
|
||||
@@ -6,6 +6,10 @@ Open-source Flutter client for MeshCore LoRa mesh networking devices.
|
||||
|
||||
MeshCore Open is a cross-platform mobile application for communicating with MeshCore LoRa mesh network devices via Bluetooth Low Energy (BLE). The app enables long-range, off-grid communication through peer-to-peer messaging, public channels, and mesh networking capabilities.
|
||||
|
||||
<a href="http://apps.obtainium.imranr.dev/redirect.html?r=obtainium://add/https://github.com/zjs81/meshcore-open">
|
||||
<img src="assets/badges/badge_obtainium.png" height="80" align="center" alt="Get it on Obtainium"/>
|
||||
</a>
|
||||
|
||||
## Screenshots
|
||||
|
||||
<table>
|
||||
@@ -21,6 +25,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
## Features
|
||||
|
||||
### Core Functionality
|
||||
|
||||
- **Direct Messaging**: Private encrypted conversations with individual contacts
|
||||
- **Public Channels**: Broadcast messages to channel subscribers on the mesh network
|
||||
- **Contact Management**: Organize contacts, track last seen times, and manage conversation history
|
||||
@@ -29,6 +34,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **Message Replies**: Thread conversations with inline reply functionality
|
||||
|
||||
### Mesh Network
|
||||
|
||||
- **Path Visualization**: View routing paths and signal quality for each contact
|
||||
- **Route Management**: Manual path overriding and automatic route rotation
|
||||
- **Signal Metrics**: Real-time SNR (Signal-to-Noise Ratio) tracking
|
||||
@@ -36,6 +42,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **Repeater Support**: Connect to and manage repeater nodes for extended range
|
||||
|
||||
### Map & Location
|
||||
|
||||
- **Live Map View**: Real-time visualization of mesh network nodes on an interactive map
|
||||
- **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
|
||||
- **Location Sharing**: Share GPS coordinates and custom markers with contacts
|
||||
@@ -43,12 +50,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
||||
|
||||
### Device Management
|
||||
|
||||
- **BLE Connection**: Scan and connect to MeshCore devices via Bluetooth
|
||||
- **Device Settings**: Configure radio parameters, power settings, and network options
|
||||
- **Battery Monitoring**: Real-time battery status with chemistry-specific voltage curves
|
||||
- **Firmware Updates**: Over-the-air firmware updates via BLE (coming soon)
|
||||
|
||||
### Repeater Hub
|
||||
|
||||
- **CLI Access**: Full command-line interface to repeater nodes
|
||||
- **Settings Management**: Configure repeater behavior, power limits, and network settings
|
||||
- **Statistics Dashboard**: View repeater traffic, connected clients, and system health
|
||||
@@ -57,6 +66,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
## Technical Details
|
||||
|
||||
### Architecture
|
||||
|
||||
- **Framework**: Flutter 3.38.5 / Dart 3.10.4
|
||||
- **State Management**: Provider pattern with ChangeNotifier
|
||||
- **BLE Protocol**: Nordic UART Service (NUS) over Bluetooth Low Energy
|
||||
@@ -64,11 +74,14 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **Encryption**: End-to-end encryption for private messages using the MeshCore protocol
|
||||
|
||||
### Platform Support
|
||||
|
||||
- ✅ **Android**: Full support (API 21+)
|
||||
- ✅ **iOS**: Full support (iOS 12+)
|
||||
- 🚧 **Desktop**: Limited support (macOS/Linux/Windows)
|
||||
- 🚧 **Web**: Under construction (Chrome)
|
||||
|
||||
### Dependencies
|
||||
|
||||
| Package | Purpose |
|
||||
|---------|---------|
|
||||
| flutter_blue_plus | Bluetooth Low Energy communication |
|
||||
@@ -84,6 +97,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
## Getting Started
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Flutter SDK 3.38.5 or later
|
||||
- Android Studio / Xcode (for mobile development)
|
||||
- A MeshCore-compatible LoRa device
|
||||
@@ -91,17 +105,20 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
### Installation
|
||||
|
||||
1. **Clone the repository**
|
||||
|
||||
```bash
|
||||
git clone https://github.com/zjs81/meshcore-open.git
|
||||
cd meshcore-open
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
|
||||
```bash
|
||||
flutter pub get
|
||||
```
|
||||
|
||||
3. **Run the app**
|
||||
|
||||
```bash
|
||||
flutter run
|
||||
```
|
||||
@@ -109,11 +126,13 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
### Building for Release
|
||||
|
||||
**Android APK:**
|
||||
|
||||
```bash
|
||||
flutter build apk --release
|
||||
```
|
||||
|
||||
**iOS:**
|
||||
|
||||
```bash
|
||||
flutter build ios --release
|
||||
```
|
||||
@@ -152,25 +171,30 @@ lib/
|
||||
## BLE Protocol
|
||||
|
||||
### Nordic UART Service (NUS)
|
||||
|
||||
- **Service UUID**: `6e400001-b5a3-f393-e0a9-e50e24dcca9e`
|
||||
- **RX Characteristic**: `6e400002-b5a3-f393-e0a9-e50e24dcca9e` (Write to device)
|
||||
- **TX Characteristic**: `6e400003-b5a3-f393-e0a9-e50e24dcca9e` (Notify from device)
|
||||
|
||||
### Device Discovery
|
||||
|
||||
Devices are discovered by scanning for BLE advertisements with the name prefix `MeshCore-`
|
||||
|
||||
### Message Format
|
||||
|
||||
Messages are transmitted as binary frames using a custom protocol optimized for LoRa transmission. See `meshcore_protocol.dart` for frame structure definitions.
|
||||
|
||||
## Configuration
|
||||
|
||||
### App Settings
|
||||
|
||||
- **Theme**: System default, light, or dark mode
|
||||
- **Notifications**: Configurable for messages, channels, and node advertisements
|
||||
- **Battery Chemistry**: Support for NMC, LiFePO4, and LiPo battery types
|
||||
- **Message Retry**: Automatic retry with configurable path clearing
|
||||
|
||||
### Device Settings
|
||||
|
||||
- **Radio Power**: Transmit power adjustment (10-30 dBm)
|
||||
- **Frequency**: LoRa frequency configuration
|
||||
- **Bandwidth**: Channel bandwidth selection
|
||||
@@ -182,22 +206,24 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
||||
This is an open-source project. Contributions are welcome!
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
- Follow the Flutter style guide
|
||||
- Use Material 3 design components
|
||||
- Write clear commit messages
|
||||
- Test on both Android and iOS before submitting PRs
|
||||
|
||||
### Code Style
|
||||
|
||||
- Prefer `StatelessWidget` with `Consumer` for reactive UI
|
||||
- Use `const` constructors where possible
|
||||
- Keep functions small and focused
|
||||
- Avoid premature abstractions
|
||||
|
||||
- Run dart format on all changes before submitting
|
||||
|
||||
## Support
|
||||
|
||||
For issues, questions, or feature requests, please open an issue on GitHub:
|
||||
https://github.com/zjs81/meshcore-open/issues
|
||||
<https://github.com/zjs81/meshcore-open/issues>
|
||||
|
||||
## Donate
|
||||
|
||||
@@ -205,6 +231,11 @@ If you find MeshCore Open useful and would like to support development, you can
|
||||
|
||||
**Solana Address:** `F15YanjZj96YTBtKJYgNa8RLQLCZkx5CEwogPWkqXeoQ`
|
||||
|
||||
|
||||
**Monero Address:** `453TxnpUqjkJtXxzdjMsrgERNkBRXEGamPbpC45ENrvKAk9tH7kZbxWF82Hz66etgDZyXFPEBU2JUEqhLeJyWt9kBvTVy5m`
|
||||
|
||||
**Bitcoin Address:** `bc1qh45x28v8dslcg4v4upmqd9g0mvc3lnyffmyzr5`
|
||||
|
||||
Your support helps maintain and improve this open-source project!
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
# TestFlight and App Store Deployment Guide
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [x] Apple Developer Account ($99/year) - [developer.apple.com](https://developer.apple.com)
|
||||
- [x] Xcode installed
|
||||
- [x] Apple Transporter app installed
|
||||
- [x] App icons ready (1024x1024px)
|
||||
- [x] Bundle ID configured: `com.monitormx.meshcoreopen`
|
||||
|
||||
## Step 1: Register Bundle Identifier
|
||||
|
||||
1. Go to [Apple Developer - Identifiers](https://developer.apple.com/account/resources/identifiers/list)
|
||||
2. Click the **"+"** button
|
||||
3. Select **"App IDs"** → Continue
|
||||
4. Select **"App"** → Continue
|
||||
5. Fill in:
|
||||
- **Description**: Meshcore Open
|
||||
- **Bundle ID**: Explicit - `com.monitormx.meshcoreopen`
|
||||
- **Capabilities**: Leave defaults (or add as needed)
|
||||
6. Click **Continue** → **Register**
|
||||
|
||||
## Step 2: Create App in App Store Connect
|
||||
|
||||
1. Go to [App Store Connect](https://appstoreconnect.apple.com)
|
||||
2. Sign in with your Apple ID
|
||||
3. Click **"My Apps"**
|
||||
4. Click the **"+"** button → **"New App"**
|
||||
5. Fill in the form:
|
||||
- **Platforms**: iOS
|
||||
- **Name**: Meshcore Open
|
||||
- **Primary Language**: English (U.S.)
|
||||
- **Bundle ID**: Select `com.monitormx.meshcoreopen` from dropdown
|
||||
- **SKU**: `meshcore-open-001` (or any unique identifier)
|
||||
- **User Access**: Full Access
|
||||
6. Click **"Create"**
|
||||
|
||||
## Step 3: Build the IPA
|
||||
|
||||
Run these commands from the project directory:
|
||||
|
||||
```bash
|
||||
# Add CocoaPods to PATH
|
||||
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||
|
||||
# Clean previous builds
|
||||
../flutter/bin/flutter clean
|
||||
|
||||
# Build IPA for App Store
|
||||
../flutter/bin/flutter build ipa
|
||||
```
|
||||
|
||||
The IPA will be created at: `build/ios/ipa/meshcore_open.ipa`
|
||||
|
||||
## Step 4: Upload to App Store Connect via Transporter
|
||||
|
||||
1. **Open Apple Transporter**
|
||||
- Launch from Applications folder
|
||||
- Sign in with your Apple ID
|
||||
|
||||
2. **Upload the IPA**
|
||||
- Drag and drop `build/ios/ipa/meshcore_open.ipa` into Transporter
|
||||
- Click **"Deliver"**
|
||||
- Wait for upload to complete (usually 1-5 minutes)
|
||||
|
||||
3. **Processing**
|
||||
- Apple will process your build (10-30 minutes)
|
||||
- You'll receive an email when processing is complete
|
||||
|
||||
## Step 5: Configure App Store Connect Metadata
|
||||
|
||||
### App Information
|
||||
1. In App Store Connect, go to your app
|
||||
2. Fill in required information:
|
||||
- **Subtitle**: Short description (30 chars max)
|
||||
- **Privacy Policy URL**: Required for Bluetooth apps
|
||||
- **Category**: Utilities or Productivity
|
||||
- **Age Rating**: Complete questionnaire
|
||||
|
||||
### App Store Listing
|
||||
1. Go to **App Store** tab
|
||||
2. Upload **Screenshots** (required):
|
||||
- iPhone 6.7" display (1290 x 2796 pixels) - At least 1 screenshot
|
||||
- iPhone 6.5" display (1242 x 2688 pixels) - At least 1 screenshot
|
||||
- Optional: iPad screenshots
|
||||
|
||||
3. Fill in **Description**:
|
||||
```
|
||||
Meshcore Open is a Flutter client for MeshCore LoRa mesh networking devices.
|
||||
|
||||
Features:
|
||||
- BLE connectivity to MeshCore devices
|
||||
- Real-time mesh network communication
|
||||
- Map visualization with OpenStreetMap
|
||||
- Community management with QR code scanning
|
||||
- Message tracking and retry system
|
||||
|
||||
Connect to your MeshCore LoRa device and start communicating over the mesh network.
|
||||
```
|
||||
|
||||
4. **Keywords**: `lora,mesh,networking,bluetooth,communication`
|
||||
5. **Support URL**: Your GitHub or website URL
|
||||
6. **Marketing URL**: (Optional)
|
||||
|
||||
### Version Information
|
||||
1. **What's New in This Version**:
|
||||
```
|
||||
Initial release of Meshcore Open
|
||||
|
||||
- BLE device connectivity
|
||||
- Mesh network messaging
|
||||
- Map integration
|
||||
- Community features
|
||||
```
|
||||
|
||||
2. **Build**: Select the uploaded build once processing completes
|
||||
|
||||
## Step 6: TestFlight Setup
|
||||
|
||||
### Internal Testing (No Review Required)
|
||||
1. Go to **TestFlight** tab in App Store Connect
|
||||
2. Click **Internal Testing** → **"+"** to create a group
|
||||
3. Name your group (e.g., "Internal Testers")
|
||||
4. Add yourself as a tester using your email
|
||||
5. Select the build you uploaded
|
||||
6. Testers will receive an email with TestFlight invitation
|
||||
|
||||
### External Testing (Requires Beta Review)
|
||||
1. Click **External Testing** → **"+"** to create a group
|
||||
2. Add build and testers
|
||||
3. Fill in **Test Information**:
|
||||
- **What to Test**: Brief description of features
|
||||
- **Feedback Email**: Your email address
|
||||
4. Click **Submit for Review**
|
||||
5. Beta review typically takes 24-48 hours
|
||||
|
||||
## Step 7: App Store Submission
|
||||
|
||||
Once you're ready for public release:
|
||||
|
||||
1. Go to **App Store** tab
|
||||
2. Complete all required metadata (if not done)
|
||||
3. Select your build
|
||||
4. Fill in **App Review Information**:
|
||||
- **Contact Information**: Your name, phone, email
|
||||
- **Demo Account**: If app requires login
|
||||
- **Notes**: Any special instructions for reviewers
|
||||
5. Answer **Export Compliance** questions:
|
||||
- Does your app use encryption? **Yes** (uses TLS/HTTPS)
|
||||
- Is encryption registration required? **No** (standard encryption)
|
||||
6. Click **Add for Review**
|
||||
7. Review summary and click **Submit to App Review**
|
||||
|
||||
## Step 8: After Submission
|
||||
|
||||
- **App Review**: Typically 24-48 hours
|
||||
- **Common Rejection Reasons**:
|
||||
- Missing privacy policy
|
||||
- Incomplete app information
|
||||
- Crashes or bugs
|
||||
- Misleading app description
|
||||
|
||||
- **If Approved**: You can release immediately or schedule a release date
|
||||
- **If Rejected**: Address issues and resubmit
|
||||
|
||||
## Updating the App
|
||||
|
||||
When you need to release an update:
|
||||
|
||||
1. **Update version** in `pubspec.yaml`:
|
||||
```yaml
|
||||
version: 0.5.0+6 # Increment version (0.5.0) and build number (+6)
|
||||
```
|
||||
|
||||
2. **Build new IPA**:
|
||||
```bash
|
||||
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||
../flutter/bin/flutter clean
|
||||
../flutter/bin/flutter build ipa
|
||||
```
|
||||
|
||||
3. **Upload via Transporter** (same process as above)
|
||||
|
||||
4. **Create new version** in App Store Connect:
|
||||
- Click **"+"** next to versions
|
||||
- Select version number
|
||||
- Update "What's New" text
|
||||
- Select new build
|
||||
- Submit for review
|
||||
|
||||
## macOS Build (Bonus)
|
||||
|
||||
To build for macOS:
|
||||
|
||||
```bash
|
||||
export PATH="/opt/homebrew/lib/ruby/gems/4.0.0/bin:$PATH"
|
||||
../flutter/bin/flutter build macos --release
|
||||
cd build/macos/Build/Products/Release
|
||||
zip -r meshcore_open-macos.zip meshcore_open.app
|
||||
```
|
||||
|
||||
Distribution:
|
||||
- Share the zip file directly
|
||||
- Users unzip and drag to Applications
|
||||
- First run: Right-click → Open (to bypass Gatekeeper)
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Build Errors
|
||||
- **CocoaPods not found**: Ensure PATH includes `/opt/homebrew/lib/ruby/gems/4.0.0/bin`
|
||||
- **No signing certificate**: Configure Team in Xcode (Signing & Capabilities)
|
||||
- **Bundle ID mismatch**: Check `ios/Runner.xcodeproj/project.pbxproj`
|
||||
|
||||
### Upload Errors
|
||||
- **No profiles found**: Create app in App Store Connect first
|
||||
- **Bundle ID not registered**: Register in Apple Developer portal
|
||||
- **Authentication failed**: Use Transporter app instead of CLI
|
||||
|
||||
### TestFlight Issues
|
||||
- **Build not appearing**: Wait 10-30 minutes for processing
|
||||
- **Can't add testers**: Check you have available slots (100 internal, 10,000 external)
|
||||
- **TestFlight crashes**: Check device logs in Xcode → Devices & Simulators
|
||||
|
||||
## Important Files
|
||||
|
||||
- **iOS IPA**: `build/ios/ipa/meshcore_open.ipa`
|
||||
- **macOS App**: `build/macos/Build/Products/Release/meshcore_open.app`
|
||||
- **Bundle ID Config**: `ios/Runner.xcodeproj/project.pbxproj`
|
||||
- **Version Info**: `pubspec.yaml`
|
||||
|
||||
## Useful Links
|
||||
|
||||
- [App Store Connect](https://appstoreconnect.apple.com)
|
||||
- [Apple Developer Portal](https://developer.apple.com/account)
|
||||
- [TestFlight Documentation](https://developer.apple.com/testflight/)
|
||||
- [App Store Review Guidelines](https://developer.apple.com/app-store/review/guidelines/)
|
||||
- [Flutter iOS Deployment](https://docs.flutter.dev/deployment/ios)
|
||||
|
||||
## Support
|
||||
|
||||
For issues with:
|
||||
- **App Store Process**: [Apple Developer Support](https://developer.apple.com/contact/)
|
||||
- **Flutter Build Issues**: [Flutter GitHub](https://github.com/flutter/flutter/issues)
|
||||
- **Meshcore Open App**: [GitHub Issues](https://github.com/wel97459/meshcore-open/issues)
|
||||
@@ -19,13 +19,13 @@ android {
|
||||
ndkVersion = flutter.ndkVersion
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_11
|
||||
targetCompatibility = JavaVersion.VERSION_11
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
isCoreLibraryDesugaringEnabled = true
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||
jvmTarget = JavaVersion.VERSION_17.toString()
|
||||
}
|
||||
|
||||
defaultConfig {
|
||||
@@ -83,5 +83,5 @@ flutter {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
<!-- Camera permission for QR code scanning -->
|
||||
<uses-permission android:name="android.permission.CAMERA"/>
|
||||
<uses-feature android:name="android.hardware.camera" android:required="false"/>
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:label="meshcore_open"
|
||||
@@ -67,5 +68,14 @@
|
||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||
<data android:mimeType="text/plain"/>
|
||||
</intent>
|
||||
<!-- URL launcher intents for opening links -->
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="http"/>
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW"/>
|
||||
<data android:scheme="https"/>
|
||||
</intent>
|
||||
</queries>
|
||||
</manifest>
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
package com.meshcore.meshcore_open
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
@@ -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 |
Generated
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770562336,
|
||||
"narHash": "sha256-ub1gpAONMFsT/GU2hV6ZWJjur8rJ6kKxdm9IlCT0j84=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "d6c71932130818840fc8fe9509cf50be8c64634f",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nixos",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
{
|
||||
description = "MeshCore Flutter Application";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = nixpkgs.legacyPackages.${system};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
buildInputs = with pkgs; [
|
||||
# Flutter and Dart
|
||||
flutter
|
||||
dart
|
||||
|
||||
# Java (required for Android development)
|
||||
jdk17
|
||||
|
||||
# Android development tools
|
||||
android-tools
|
||||
gradle
|
||||
|
||||
# For the shell hook to set up the environment for Flutter development
|
||||
gtk3
|
||||
glib
|
||||
sysprof
|
||||
libclang
|
||||
cmake
|
||||
ninja
|
||||
pkg-config
|
||||
libdatrie
|
||||
|
||||
# Additional tools for installing Android SDK if not present
|
||||
curl
|
||||
unzip
|
||||
];
|
||||
|
||||
shellHook = ''
|
||||
echo "MeshCore Flutter Development Environment"
|
||||
export PKG_CONFIG_PATH="${pkgs.gtk3}/lib/pkgconfig:${pkgs.glib}/lib/pkgconfig:${pkgs.sysprof}/lib/pkgconfig:$PKG_CONFIG_PATH"
|
||||
export LD_LIBRARY_PATH="${pkgs.lib.makeLibraryPath [pkgs.gtk3 pkgs.glib pkgs.sysprof pkgs.libdatrie]}:$LD_LIBRARY_PATH"
|
||||
export CMAKE_INSTALL_PREFIX="$PWD/build/bundle"
|
||||
|
||||
# Setup Android SDK in home directory (standard location)
|
||||
export ANDROID_HOME="$HOME/Android/Sdk"
|
||||
export ANDROID_SDK_ROOT="$ANDROID_HOME"
|
||||
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools/bin:$PATH"
|
||||
|
||||
echo "Android SDK: $ANDROID_HOME"
|
||||
echo ""
|
||||
|
||||
# Check if Android SDK exists and offer to download if not
|
||||
if [ ! -d "$ANDROID_HOME" ]; then
|
||||
echo "WARNING: Android SDK not found at $ANDROID_HOME"
|
||||
echo ""
|
||||
echo "To download and set up the Android SDK, run this command:"
|
||||
echo ""
|
||||
cat << 'EOF'
|
||||
mkdir -p ~/Android/Sdk && cd ~/Android/Sdk && \
|
||||
curl -o cmdline-tools.zip ${if pkgs.stdenv.isDarwin then "https://dl.google.com/android/repository/commandlinetools-mac-10406996_latest.zip" else "https://dl.google.com/android/repository/commandlinetools-linux-10406996_latest.zip"} && \
|
||||
unzip -q cmdline-tools.zip && \
|
||||
mkdir -p cmdline-tools/latest && \
|
||||
mv cmdline-tools/* cmdline-tools/latest/ 2>/dev/null || echo "Warning: failed to move Android cmdline-tools into 'latest' directory; please check your SDK layout." >&2 && \
|
||||
rm cmdline-tools.zip && \
|
||||
cd cmdline-tools/latest/bin && \
|
||||
yes | ./sdkmanager --sdk_root=~/Android/Sdk 'platform-tools' && \
|
||||
echo "Android SDK setup complete!"
|
||||
EOF
|
||||
echo ""
|
||||
echo "Then run 'flutter doctor' again to verify."
|
||||
echo ""
|
||||
else
|
||||
echo "Android SDK found at $ANDROID_HOME"
|
||||
fi
|
||||
|
||||
echo "To check that everything is set up correctly, run 'flutter doctor' and ensure there are no issues."
|
||||
'';
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
|
||||
#include "Generated.xcconfig"
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
platform :ios, '12.0'
|
||||
platform :ios, '15.5'
|
||||
|
||||
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
|
||||
|
||||
|
||||
@@ -0,0 +1,145 @@
|
||||
PODS:
|
||||
- Flutter (1.0.0)
|
||||
- flutter_blue_plus_darwin (0.0.2):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- flutter_foreground_task (0.0.1):
|
||||
- Flutter
|
||||
- flutter_local_notifications (0.0.1):
|
||||
- Flutter
|
||||
- GoogleDataTransport (10.1.0):
|
||||
- nanopb (~> 3.30910.0)
|
||||
- PromisesObjC (~> 2.4)
|
||||
- GoogleMLKit/BarcodeScanning (7.0.0):
|
||||
- GoogleMLKit/MLKitCore
|
||||
- MLKitBarcodeScanning (~> 6.0.0)
|
||||
- GoogleMLKit/MLKitCore (7.0.0):
|
||||
- MLKitCommon (~> 12.0.0)
|
||||
- GoogleToolboxForMac/Defines (4.2.1)
|
||||
- GoogleToolboxForMac/Logger (4.2.1):
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (4.2.1)":
|
||||
- GoogleToolboxForMac/Defines (= 4.2.1)
|
||||
- GoogleUtilities/Environment (8.1.0):
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Logger (8.1.0):
|
||||
- GoogleUtilities/Environment
|
||||
- GoogleUtilities/Privacy
|
||||
- GoogleUtilities/Privacy (8.1.0)
|
||||
- GoogleUtilities/UserDefaults (8.1.0):
|
||||
- GoogleUtilities/Logger
|
||||
- GoogleUtilities/Privacy
|
||||
- GTMSessionFetcher/Core (3.5.0)
|
||||
- MLImage (1.0.0-beta6)
|
||||
- MLKitBarcodeScanning (6.0.0):
|
||||
- MLKitCommon (~> 12.0)
|
||||
- MLKitVision (~> 8.0)
|
||||
- MLKitCommon (12.0.0):
|
||||
- GoogleDataTransport (~> 10.0)
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GoogleUtilities/Logger (~> 8.0)
|
||||
- GoogleUtilities/UserDefaults (~> 8.0)
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLKitVision (8.0.0):
|
||||
- GoogleToolboxForMac/Logger (< 5.0, >= 4.2.1)
|
||||
- "GoogleToolboxForMac/NSData+zlib (< 5.0, >= 4.2.1)"
|
||||
- GTMSessionFetcher/Core (< 4.0, >= 3.3.2)
|
||||
- MLImage (= 1.0.0-beta6)
|
||||
- MLKitCommon (~> 12.0)
|
||||
- mobile_scanner (6.0.2):
|
||||
- Flutter
|
||||
- GoogleMLKit/BarcodeScanning (~> 7.0.0)
|
||||
- nanopb (3.30910.0):
|
||||
- nanopb/decode (= 3.30910.0)
|
||||
- nanopb/encode (= 3.30910.0)
|
||||
- nanopb/decode (3.30910.0)
|
||||
- nanopb/encode (3.30910.0)
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- PromisesObjC (2.4.0)
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- Flutter (from `Flutter`)
|
||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
||||
- flutter_foreground_task (from `.symlinks/plugins/flutter_foreground_task/ios`)
|
||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
||||
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/ios`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
- GoogleDataTransport
|
||||
- GoogleMLKit
|
||||
- GoogleToolboxForMac
|
||||
- GoogleUtilities
|
||||
- GTMSessionFetcher
|
||||
- MLImage
|
||||
- MLKitBarcodeScanning
|
||||
- MLKitCommon
|
||||
- MLKitVision
|
||||
- nanopb
|
||||
- PromisesObjC
|
||||
|
||||
EXTERNAL SOURCES:
|
||||
Flutter:
|
||||
:path: Flutter
|
||||
flutter_blue_plus_darwin:
|
||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
||||
flutter_foreground_task:
|
||||
:path: ".symlinks/plugins/flutter_foreground_task/ios"
|
||||
flutter_local_notifications:
|
||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
||||
mobile_scanner:
|
||||
:path: ".symlinks/plugins/mobile_scanner/ios"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
||||
sqflite_darwin:
|
||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||
flutter_local_notifications: 395056b3175ba4f08480a7c5de30cd36d69827e4
|
||||
GoogleDataTransport: aae35b7ea0c09004c3797d53c8c41f66f219d6a7
|
||||
GoogleMLKit: eff9e23ec1d90ea4157a1ee2e32a4f610c5b3318
|
||||
GoogleToolboxForMac: d1a2cbf009c453f4d6ded37c105e2f67a32206d8
|
||||
GoogleUtilities: 00c88b9a86066ef77f0da2fab05f65d7768ed8e1
|
||||
GTMSessionFetcher: 5aea5ba6bd522a239e236100971f10cb71b96ab6
|
||||
MLImage: 0ad1c5f50edd027672d8b26b0fee78a8b4a0fc56
|
||||
MLKitBarcodeScanning: 0a3064da0a7f49ac24ceb3cb46a5bc67496facd2
|
||||
MLKitCommon: 07c2c33ae5640e5380beaaa6e4b9c249a205542d
|
||||
MLKitVision: 45e79d68845a2de77e2dd4d7f07947f0ed157b0e
|
||||
mobile_scanner: af8f71879eaba2bbcb4d86c6a462c3c0e7f23036
|
||||
nanopb: fad817b59e0457d11a5dfbde799381cd727c1275
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 570da2a631486c6bd6496bed1e605e63e2471be5
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
@@ -14,6 +14,7 @@
|
||||
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
|
||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 4268181FCF3E12817B700E9C /* libPods-Runner.a */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXContainerItemProxy section */
|
||||
@@ -42,9 +43,13 @@
|
||||
/* Begin PBXFileReference section */
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
|
||||
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
4268181FCF3E12817B700E9C /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
|
||||
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -62,6 +67,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@@ -94,6 +100,8 @@
|
||||
97C146F01CF9000F007C117D /* Runner */,
|
||||
97C146EF1CF9000F007C117D /* Products */,
|
||||
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||
DEE6F094D3B70E76087722E1 /* Pods */,
|
||||
DAE613E34DF694C2E33B64C7 /* Frameworks */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
@@ -121,6 +129,25 @@
|
||||
path = Runner;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DAE613E34DF694C2E33B64C7 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
4268181FCF3E12817B700E9C /* libPods-Runner.a */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
DEE6F094D3B70E76087722E1 /* Pods */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
40AC50CE3E1D4278E82498CF /* Pods-Runner.debug.xcconfig */,
|
||||
24A76623340E493BD4C25C5C /* Pods-Runner.release.xcconfig */,
|
||||
718BC7DCCFC5C370705C12E5 /* Pods-Runner.profile.xcconfig */,
|
||||
);
|
||||
name = Pods;
|
||||
path = Pods;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXGroup section */
|
||||
|
||||
/* Begin PBXNativeTarget section */
|
||||
@@ -145,12 +172,14 @@
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||
buildPhases = (
|
||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */,
|
||||
9740EEB61CF901F6004384FC /* Run Script */,
|
||||
97C146EA1CF9000F007C117D /* Sources */,
|
||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||
97C146EC1CF9000F007C117D /* Resources */,
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
@@ -253,6 +282,45 @@
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||
};
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
|
||||
);
|
||||
name = "[CP] Copy Pods Resources";
|
||||
outputFileListPaths = (
|
||||
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
|
||||
"${PODS_ROOT}/Manifest.lock",
|
||||
);
|
||||
name = "[CP] Check Pods Manifest.lock";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
|
||||
showEnvVarsInLog = 0;
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -368,7 +436,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
@@ -384,7 +452,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -401,7 +469,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -416,7 +484,7 @@
|
||||
CURRENT_PROJECT_VERSION = 1;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen.RunnerTests;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen.RunnerTests;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
|
||||
@@ -547,7 +615,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
@@ -569,7 +637,7 @@
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.meshcore.meshcoreOpen;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.monitormx.meshcoreopen;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||
SWIFT_VERSION = 5.0;
|
||||
|
||||
+3
@@ -4,4 +4,7 @@
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
</FileRef>
|
||||
<FileRef
|
||||
location = "group:Pods/Pods.xcodeproj">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
||||
@@ -55,5 +55,10 @@
|
||||
<string>This app uses Bluetooth to communicate with MeshCore devices.</string>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>This app uses the camera to scan QR codes for joining communities.</string>
|
||||
<key>LSApplicationQueriesSchemes</key>
|
||||
<array>
|
||||
<string>http</string>
|
||||
<string>https</string>
|
||||
</array>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
+1841
-376
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,71 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../services/usb_serial_service.dart';
|
||||
|
||||
/// Manages USB serial transport for MeshCore devices.
|
||||
///
|
||||
/// Owns the [UsbSerialService] and USB-specific connection state.
|
||||
/// The main [MeshCoreConnector] delegates all USB operations here.
|
||||
class MeshCoreUsbManager {
|
||||
MeshCoreUsbManager();
|
||||
|
||||
final UsbSerialService _service = UsbSerialService();
|
||||
AppDebugLogService? _debugLog;
|
||||
String? _activePortKey;
|
||||
String? _activePortLabel;
|
||||
|
||||
// --- Getters ---
|
||||
String? get activePortKey => _activePortKey;
|
||||
String? get activePortDisplayLabel => _activePortLabel ?? _activePortKey;
|
||||
bool get isConnected => _service.isConnected;
|
||||
Stream<Uint8List> get frameStream => _service.frameStream;
|
||||
|
||||
// --- Configuration ---
|
||||
Future<List<String>> listPorts() => _service.listPorts();
|
||||
|
||||
void setRequestPortLabel(String label) =>
|
||||
_service.setRequestPortLabel(label);
|
||||
|
||||
void setFallbackDeviceName(String label) =>
|
||||
_service.setFallbackDeviceName(label);
|
||||
|
||||
void setDebugLogService(AppDebugLogService? service) {
|
||||
_debugLog = service;
|
||||
_service.setDebugLogService(service);
|
||||
}
|
||||
|
||||
// --- Connection lifecycle ---
|
||||
Future<void> connect({required String portName, int baudRate = 115200}) async {
|
||||
_debugLog?.info(
|
||||
'UsbManager.connect: portName=$portName baud=$baudRate',
|
||||
tag: 'USB',
|
||||
);
|
||||
await _service.connect(portName: portName, baudRate: baudRate);
|
||||
_activePortKey = _service.activePortKey ?? portName;
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? portName;
|
||||
_debugLog?.info(
|
||||
'UsbManager.connect: done, key=$_activePortKey label=$_activePortLabel',
|
||||
tag: 'USB',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> disconnect() async {
|
||||
_debugLog?.info('UsbManager.disconnect', tag: 'USB');
|
||||
await _service.disconnect();
|
||||
_activePortKey = null;
|
||||
_activePortLabel = null;
|
||||
}
|
||||
|
||||
Future<void> write(Uint8List data) => _service.write(data);
|
||||
|
||||
// --- Label management ---
|
||||
void updateConnectedLabel(String selfName) {
|
||||
_service.updateConnectedLabel(selfName);
|
||||
_activePortLabel = _service.activePortDisplayLabel ?? _activePortLabel;
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
_service.dispose();
|
||||
}
|
||||
}
|
||||
@@ -13,15 +13,35 @@ class BufferReader {
|
||||
int readByte() => readBytes(1)[0];
|
||||
|
||||
Uint8List readBytes(int count) {
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to read $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
);
|
||||
}
|
||||
final data = _buffer.sublist(_pointer, _pointer + count);
|
||||
_pointer += count;
|
||||
return data;
|
||||
}
|
||||
|
||||
void skipBytes(int count) {
|
||||
if (_pointer + count > _buffer.length) {
|
||||
throw RangeError(
|
||||
'Attempted to skip $count bytes at offset $_pointer, but only $remaining bytes remaining in buffer of length ${_buffer.length}',
|
||||
);
|
||||
}
|
||||
_pointer += count;
|
||||
}
|
||||
|
||||
Uint8List readRemainingBytes() => readBytes(remaining);
|
||||
|
||||
String readString() =>
|
||||
utf8.decode(readRemainingBytes(), allowMalformed: true);
|
||||
String readString() {
|
||||
final value = readRemainingBytes();
|
||||
try {
|
||||
return utf8.decode(Uint8List.fromList(value), allowMalformed: true);
|
||||
} catch (e) {
|
||||
return String.fromCharCodes(value); // Latin-1 fallback
|
||||
}
|
||||
}
|
||||
|
||||
String readCString(int maxLength) {
|
||||
final value = <int>[];
|
||||
@@ -98,6 +118,27 @@ class BufferWriter {
|
||||
}
|
||||
writeBytes(bytes);
|
||||
}
|
||||
|
||||
void writeHex(String hex) {
|
||||
writeBytes(hex2Uint8List(hex));
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -127,11 +168,15 @@ const int cmdSendStatusReq = 27;
|
||||
const int cmdGetContactByKey = 30;
|
||||
const int cmdGetChannel = 31;
|
||||
const int cmdSetChannel = 32;
|
||||
const int cmdGetRadioSettings = 57;
|
||||
const int cmdSendTracePath = 36;
|
||||
const int cmdSetOtherParams = 38;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdGetTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
const int cmdGetAutoAddConfig = 59;
|
||||
|
||||
// Text message types
|
||||
const int txtTypePlain = 0;
|
||||
@@ -142,7 +187,7 @@ const int reqTypeGetStatus = 0x01;
|
||||
const int reqTypeKeepAlive = 0x02;
|
||||
const int reqTypeGetTelemetry = 0x03;
|
||||
const int reqTypeGetAccessList = 0x05;
|
||||
const int reqTypeGetNeighbours = 0x06;
|
||||
const int reqTypeGetNeighbors = 0x06;
|
||||
|
||||
// Repeater response codes
|
||||
const int respServerLoginOk = 0;
|
||||
@@ -159,13 +204,14 @@ const int respCodeContactMsgRecv = 7;
|
||||
const int respCodeChannelMsgRecv = 8;
|
||||
const int respCodeCurrTime = 9;
|
||||
const int respCodeNoMoreMessages = 10;
|
||||
const int respCodeExportContact = 11;
|
||||
const int respCodeBattAndStorage = 12;
|
||||
const int respCodeDeviceInfo = 13;
|
||||
const int respCodeContactMsgRecvV3 = 16;
|
||||
const int respCodeChannelMsgRecvV3 = 17;
|
||||
const int respCodeChannelInfo = 18;
|
||||
const int respCodeRadioSettings = 25;
|
||||
const int respCodeCustomVars = 21;
|
||||
const int respCodeAutoAddConfig = 25;
|
||||
|
||||
// Push codes (async from device)
|
||||
const int pushCodeAdvert = 0x80;
|
||||
@@ -176,6 +222,7 @@ const int pushCodeLoginSuccess = 0x85;
|
||||
const int pushCodeLoginFail = 0x86;
|
||||
const int pushCodeStatusResponse = 0x87;
|
||||
const int pushCodeLogRxData = 0x88;
|
||||
const int pushCodeTraceData = 0x89;
|
||||
const int pushCodeNewAdvert = 0x8A;
|
||||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
@@ -186,6 +233,42 @@ const int advTypeRepeater = 2;
|
||||
const int advTypeRoom = 3;
|
||||
const int advTypeSensor = 4;
|
||||
|
||||
// Payload Types
|
||||
const int payloadTypeREQ =
|
||||
0x00; // request (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeRESPONSE =
|
||||
0x01; // response to REQ or ANON_REQ (prefixed with dest/src hashes, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeTXTMSG =
|
||||
0x02; // a plain text message (prefixed with dest/src hashes, MAC) (enc data: timestamp, text)
|
||||
const int payloadTypeACK = 0x03; // a simple ack
|
||||
const int payloadTypeADVERT = 0x04; // a node advertising its Identity
|
||||
const int payloadTypeGRPTXT =
|
||||
0x05; // an (unverified) group text message (prefixed with channel hash, MAC) (enc data: timestamp, "name: msg")
|
||||
const int payloadTypeGRPDATA =
|
||||
0x06; // an (unverified) group datagram (prefixed with channel hash, MAC) (enc data: timestamp, blob)
|
||||
const int payloadTypeANONREQ =
|
||||
0x07; // generic request (prefixed with dest_hash, ephemeral pub_key, MAC) (enc data: ...)
|
||||
const int payloadTypePATH =
|
||||
0x08; // returned path (prefixed with dest/src hashes, MAC) (enc data: path, extra)
|
||||
const int payloadTypeTRACE = 0x09; // trace a path, collecting SNI for each hop
|
||||
const int payloadTypeMULTIPART = 0x0A; // packet is one of a set of packets
|
||||
const int payloadTypeCONTROL = 0x0B; // a control/discovery packet
|
||||
//...
|
||||
const int payloadTypeRawCustom =
|
||||
0x0F; // custom packet as raw bytes, for applications with custom encryption, payloads, etc
|
||||
|
||||
//auto-add flags
|
||||
const int autoAddOverwriteOldestFlag =
|
||||
1 << 0; // 0x01 - overwrite oldest non-favourite when full
|
||||
const int autoAddChatFlag =
|
||||
1 << 1; // 0x02 - auto-add Chat (Companion) (ADV_TYPE_CHAT)
|
||||
const int autoAddRepeaterFlag =
|
||||
1 << 2; // 0x04 - auto-add Repeater (ADV_TYPE_REPEATER)
|
||||
const int autoAddRoomServerFlag =
|
||||
1 << 3; // 0x08 - auto-add Room Server (ADV_TYPE_ROOM)
|
||||
const int autoAddSensorFlag =
|
||||
1 << 4; // 0x10 - auto-add Sensor (ADV_TYPE_SENSOR)
|
||||
|
||||
// Sizes
|
||||
const int pubKeySize = 32;
|
||||
const int maxPathSize = 64;
|
||||
@@ -195,8 +278,10 @@ const int maxFrameSize = 172;
|
||||
const int appProtocolVersion = 3;
|
||||
// Matches firmware MAX_TEXT_LEN (10 * CIPHER_BLOCK_SIZE).
|
||||
const int maxTextPayloadBytes = 160;
|
||||
const int _sendTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 6 + 1;
|
||||
const int _sendChannelTextMsgOverheadBytes = 1 + 1 + 1 + 4 + 1;
|
||||
const int _sendTextMsgOverheadBytes =
|
||||
1 + 1 + 1 + 4 + 6 + 1 + 2; // +2 safety margin
|
||||
const int _sendChannelTextMsgOverheadBytes =
|
||||
1 + 1 + 1 + 4 + 1 + 2; // +2 safety margin
|
||||
|
||||
int maxContactMessageBytes() {
|
||||
final byFrame = maxFrameSize - _sendTextMsgOverheadBytes;
|
||||
@@ -227,13 +312,14 @@ int _minPositive(int a, int b) {
|
||||
const int contactPubKeyOffset = 1;
|
||||
const int contactTypeOffset = 33;
|
||||
const int contactFlagsOffset = 34;
|
||||
const int contactFlagFavorite = 0x01;
|
||||
const int contactPathLenOffset = 35;
|
||||
const int contactPathOffset = 36;
|
||||
const int contactNameOffset = 100;
|
||||
const int contactTimestampOffset = 132;
|
||||
const int contactLatOffset = 136;
|
||||
const int contactLonOffset = 140;
|
||||
const int contactLastmodOffset = 144;
|
||||
const int contactLastModOffset = 144;
|
||||
const int contactFrameSize = 148;
|
||||
|
||||
// Message frame offsets
|
||||
@@ -522,18 +608,29 @@ Uint8List buildSetChannelFrame(int channelIndex, String name, Uint8List psk) {
|
||||
}
|
||||
|
||||
// Build CMD_SET_RADIO_PARAMS frame
|
||||
// Format: [cmd][freq x4][bw x4][sf][cr]
|
||||
// Format: [cmd][freq x4][bw x4][sf][cr] (pre-v9)
|
||||
// [cmd][freq x4][bw x4][sf][cr][repeat] (firmware v9+)
|
||||
// freq: frequency in Hz (300000-2500000)
|
||||
// bw: bandwidth in Hz (7000-500000)
|
||||
// sf: spreading factor (5-12)
|
||||
// cr: coding rate (5-8)
|
||||
Uint8List buildSetRadioParamsFrame(int freqHz, int bwHz, int sf, int cr) {
|
||||
// clientRepeat: enable off-grid packet repeat (firmware v9+, omit for older)
|
||||
Uint8List buildSetRadioParamsFrame(
|
||||
int freqHz,
|
||||
int bwHz,
|
||||
int sf,
|
||||
int cr, {
|
||||
bool? clientRepeat,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSetRadioParams);
|
||||
writer.writeUInt32LE(freqHz);
|
||||
writer.writeUInt32LE(bwHz);
|
||||
writer.writeByte(sf);
|
||||
writer.writeByte(cr);
|
||||
if (clientRepeat != null) {
|
||||
writer.writeByte(clientRepeat ? 1 : 0);
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
@@ -600,16 +697,15 @@ Uint8List buildGetContactByKeyFrame(Uint8List pubKey) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build CMD_GET_RADIO_SETTINGS frame
|
||||
Uint8List buildGetRadioSettingsFrame() {
|
||||
return Uint8List.fromList([cmdGetRadioSettings]);
|
||||
}
|
||||
|
||||
//Build CMD_GET_CUSTOM_VARS frame
|
||||
Uint8List buildGetCustomVarsFrame() {
|
||||
return Uint8List.fromList([cmdGetCustomVar]);
|
||||
}
|
||||
|
||||
Uint8List buildGetAutoAddFlagsFrame() {
|
||||
return Uint8List.fromList([cmdGetAutoAddConfig]);
|
||||
}
|
||||
|
||||
// Calculate LoRa airtime for a packet
|
||||
// Based on Semtech SX127x datasheet formula
|
||||
// Returns airtime in milliseconds
|
||||
@@ -708,3 +804,83 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
//Build a trace request frame
|
||||
//[cmd][tag x4][auth x4][flag][payload]
|
||||
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendTracePath);
|
||||
writer.writeUInt32LE(tag);
|
||||
writer.writeUInt32LE(auth);
|
||||
writer.writeByte(flag);
|
||||
if (payload != null && payload.isNotEmpty) {
|
||||
writer.writeBytes(payload);
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build a export contact frame
|
||||
// [cmd][pub_key x32 / if empty exports your contact info]
|
||||
Uint8List buildExportContactFrame(Uint8List pubKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdExportContact);
|
||||
writer.writeBytes(pubKey);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build a import contact frame
|
||||
// [cmd][contact_frame x98+]
|
||||
Uint8List buildImportContactFrame(Uint8List contactFrame) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdImportContact);
|
||||
writer.writeBytes(contactFrame);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
// Build a export contact frame
|
||||
// [cmd][pub_key x32]
|
||||
Uint8List buildZeroHopContact(Uint8List pubKey) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdShareContact);
|
||||
writer.writeBytes(pubKey);
|
||||
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();
|
||||
}
|
||||
|
||||
+179
-163
@@ -1,4 +1,6 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class CayenneLpp {
|
||||
@@ -26,9 +28,11 @@ class CayenneLpp {
|
||||
static const int lppUnixTime = 133; // 4 bytes, unsigned
|
||||
static const int lppGyrometer = 134; // 2 bytes per axis, 0.01 °/s
|
||||
static const int lppColour = 135; // 1 byte per RGB Color
|
||||
static const int lppGps = 136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
||||
static const int lppGps =
|
||||
136; // 3 byte lon/lat 0.0001 °, 3 bytes alt 0.01 meter
|
||||
static const int lppSwitch = 142; // 1 byte, 0/1
|
||||
static const int lppPolyline = 240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
||||
static const int lppPolyline =
|
||||
240; // 1 byte size, 1 byte delta factor, 3 byte lon/lat 0.0001° * factor, n (size-8) bytes deltas
|
||||
|
||||
final BufferWriter _writer = BufferWriter();
|
||||
|
||||
@@ -82,180 +86,192 @@ class CayenneLpp {
|
||||
static List<Map<String, dynamic>> parse(Uint8List bytes) {
|
||||
final buffer = BufferReader(bytes);
|
||||
final telemetry = <Map<String, dynamic>>[];
|
||||
try {
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppLuminosity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPresence:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppTemperature:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppCurrent:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppConcentration:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPower:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'latitude': buffer.readInt24BE() / 10000,
|
||||
'longitude': buffer.readInt24BE() / 10000,
|
||||
'altitude': buffer.readInt24BE() / 100,
|
||||
},
|
||||
});
|
||||
break;
|
||||
default:
|
||||
return telemetry;
|
||||
switch (type) {
|
||||
case lppGenericSensor:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt32BE(),
|
||||
});
|
||||
break;
|
||||
case lppLuminosity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPresence:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppTemperature:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppRelativeHumidity:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8() / 2,
|
||||
});
|
||||
break;
|
||||
case lppBarometricPressure:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE() / 10,
|
||||
});
|
||||
break;
|
||||
case lppVoltage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 100,
|
||||
});
|
||||
break;
|
||||
case lppCurrent:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readInt16BE() / 1000,
|
||||
});
|
||||
break;
|
||||
case lppPercentage:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt8(),
|
||||
});
|
||||
break;
|
||||
case lppConcentration:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppPower:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': buffer.readUInt16BE(),
|
||||
});
|
||||
break;
|
||||
case lppGps:
|
||||
telemetry.add({
|
||||
'channel': channel,
|
||||
'type': type,
|
||||
'value': {
|
||||
'latitude': buffer.readInt24BE() / 10000,
|
||||
'longitude': buffer.readInt24BE() / 10000,
|
||||
'altitude': buffer.readInt24BE() / 100,
|
||||
},
|
||||
});
|
||||
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) {
|
||||
final buffer = BufferReader(bytes);
|
||||
final Map<int, Map<String, dynamic>> channels = {};
|
||||
try {
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
|
||||
while (buffer.remaining >= 2) {
|
||||
final channel = buffer.readUInt8();
|
||||
final type = buffer.readUInt8();
|
||||
// Optional: stop on padding (00 00)
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Optional: stop on padding (00 00)
|
||||
if (channel == 0 && type == 0) {
|
||||
break;
|
||||
final channelData = channels.putIfAbsent(
|
||||
channel,
|
||||
() => {'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(channel, () => {
|
||||
'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:
|
||||
// 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;
|
||||
} catch (e) {
|
||||
// Handle parsing errors, possibly due to malformed data
|
||||
appLogger.error('Error parsing Cayenne LPP data: $e');
|
||||
return <
|
||||
Map<String, dynamic>
|
||||
>[]; // Return an empty list on error to avoid crashing the app
|
||||
}
|
||||
|
||||
final List<Map<String, dynamic>> channelsOut = channels.values.toList();
|
||||
channelsOut.sort((a, b) => a['channel'].compareTo(b['channel']));
|
||||
return channelsOut;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatScrollController extends ScrollController {
|
||||
final ValueNotifier<bool> showJumpToBottom = ValueNotifier(false);
|
||||
VoidCallback? onScrollNearTop;
|
||||
|
||||
static const _bottomThreshold = 100.0;
|
||||
static const _topThreshold = 50.0;
|
||||
|
||||
ChatScrollController() {
|
||||
addListener(_handleScroll);
|
||||
}
|
||||
|
||||
void _handleScroll() {
|
||||
if (!hasClients) return;
|
||||
final pos = position;
|
||||
|
||||
// With reverse: true, position 0 is bottom, maxScrollExtent is top
|
||||
// Show jump button when scrolled away from bottom (position > threshold)
|
||||
final isAtBottom = pos.pixels <= _bottomThreshold;
|
||||
if (showJumpToBottom.value == isAtBottom) {
|
||||
showJumpToBottom.value = !isAtBottom;
|
||||
}
|
||||
|
||||
// Pagination trigger when scrolled near top (maxScrollExtent)
|
||||
if (pos.pixels >= pos.maxScrollExtent - _topThreshold) {
|
||||
onScrollNearTop?.call();
|
||||
}
|
||||
}
|
||||
|
||||
void jumpToBottom() {
|
||||
if (hasClients && position.maxScrollExtent > 0) {
|
||||
animateTo(
|
||||
0, // With reverse: true, position 0 is bottom
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void handleKeyboardOpen() {
|
||||
// Simple: just scroll to bottom when keyboard opens
|
||||
if (hasClients) {
|
||||
animateTo(
|
||||
0, // With reverse: true, position 0 is bottom
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void scrollToBottomIfAtBottom() {
|
||||
// Only scroll if jump button is NOT showing (i.e., already at bottom)
|
||||
if (!showJumpToBottom.value && hasClients && position.maxScrollExtent > 0) {
|
||||
animateTo(
|
||||
0, // With reverse: true, position 0 is bottom
|
||||
duration: const Duration(milliseconds: 200),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
showJumpToBottom.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
|
||||
class LinkHandler {
|
||||
static Future<void> handleLinkTap(BuildContext context, String url) async {
|
||||
// Show confirmation dialog
|
||||
final shouldOpen = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.chat_openLink),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.chat_openLinkConfirmation,
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: SelectableText(
|
||||
url,
|
||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context, false),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.pop(context, true),
|
||||
child: Text(context.l10n.chat_open),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldOpen != true) return;
|
||||
|
||||
// Launch URL
|
||||
try {
|
||||
final uri = Uri.parse(url);
|
||||
if (!await launchUrl(uri, mode: LaunchMode.externalApplication)) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_couldNotOpenLink(url)),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.chat_invalidLink),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,68 @@
|
||||
class ReactionInfo {
|
||||
final String targetMessageId;
|
||||
final String emoji;
|
||||
final String? reactionKey; // Lightweight key for deduplication: timestamp_senderPrefix
|
||||
import '../widgets/emoji_picker.dart';
|
||||
|
||||
ReactionInfo({
|
||||
required this.targetMessageId,
|
||||
required this.emoji,
|
||||
this.reactionKey,
|
||||
});
|
||||
class ReactionInfo {
|
||||
final String targetHash;
|
||||
final String emoji;
|
||||
|
||||
ReactionInfo({required this.targetHash, required this.emoji});
|
||||
}
|
||||
|
||||
class ReactionHelper {
|
||||
/// Parse reaction format: r:[messageId]:[emoji]
|
||||
/// Supports both old format (full messageId) and new format (timestamp_senderPrefix)
|
||||
static List<String>? _cachedEmojis;
|
||||
|
||||
/// Combined list of all reaction emojis in fixed order.
|
||||
/// Order must stay stable for index compatibility.
|
||||
static List<String> get reactionEmojis {
|
||||
return _cachedEmojis ??= [
|
||||
...EmojiPicker.quickEmojis,
|
||||
...EmojiPicker.smileys,
|
||||
...EmojiPicker.gestures,
|
||||
...EmojiPicker.hearts,
|
||||
...EmojiPicker.objects,
|
||||
];
|
||||
}
|
||||
|
||||
/// Convert emoji to 2-char hex index. Returns null if emoji not in list.
|
||||
static String? emojiToIndex(String emoji) {
|
||||
final idx = reactionEmojis.indexOf(emoji);
|
||||
if (idx < 0) return null;
|
||||
return idx.toRadixString(16).padLeft(2, '0');
|
||||
}
|
||||
|
||||
/// Convert 2-char hex index to emoji. Returns null if invalid index.
|
||||
static String? indexToEmoji(String hexIndex) {
|
||||
final idx = int.tryParse(hexIndex, radix: 16);
|
||||
if (idx == null || idx < 0 || idx >= reactionEmojis.length) return null;
|
||||
return reactionEmojis[idx];
|
||||
}
|
||||
|
||||
/// Compute a 4-char hex hash for a message reaction.
|
||||
/// Hash input: timestampSeconds + [senderName] + first 5 chars of text
|
||||
/// For 1:1 chats, senderName can be null (sender is implicit).
|
||||
static String computeReactionHash(
|
||||
int timestampSeconds,
|
||||
String? senderName,
|
||||
String text,
|
||||
) {
|
||||
final first5 = text.length >= 5 ? text.substring(0, 5) : text;
|
||||
final input = senderName != null
|
||||
? '$timestampSeconds$senderName$first5'
|
||||
: '$timestampSeconds$first5';
|
||||
// Use hashCode and take lower 16 bits, format as 4 hex chars
|
||||
final hash = input.hashCode & 0xFFFF;
|
||||
return hash.toRadixString(16).padLeft(4, '0');
|
||||
}
|
||||
|
||||
/// Parse reaction format: r:HASH:INDEX (where INDEX is 2-char hex emoji index)
|
||||
/// Returns null if text is not a valid reaction format
|
||||
static ReactionInfo? parseReaction(String text) {
|
||||
final regex = RegExp(r'^r:([^:]+):(.+)$');
|
||||
final regex = RegExp(r'^r:([0-9a-f]{4}):([0-9a-f]{2})$');
|
||||
final match = regex.firstMatch(text);
|
||||
if (match == null) return null;
|
||||
|
||||
final targetId = match.group(1)!;
|
||||
final emoji = match.group(2)!;
|
||||
final emoji = indexToEmoji(match.group(2)!);
|
||||
if (emoji == null) return null;
|
||||
|
||||
// Extract reaction key for deduplication
|
||||
// If targetId is in new format (timestamp_senderPrefix), use it directly
|
||||
// Otherwise, extract timestamp from old format (timestamp_nameHash_textHash)
|
||||
String? reactionKey;
|
||||
if (targetId.contains('_')) {
|
||||
final parts = targetId.split('_');
|
||||
if (parts.length >= 2) {
|
||||
// New format: timestamp_senderPrefix, or old format with at least timestamp
|
||||
reactionKey = '${parts[0]}_${parts[1]}';
|
||||
}
|
||||
}
|
||||
|
||||
return ReactionInfo(
|
||||
targetMessageId: targetId,
|
||||
emoji: emoji,
|
||||
reactionKey: reactionKey,
|
||||
);
|
||||
}
|
||||
|
||||
/// Generate a lightweight reaction key for a message
|
||||
/// Format: r:[timestamp]_[senderPrefix]:[emoji]
|
||||
static String buildReactionText(String timestamp, String senderPrefix, String emoji) {
|
||||
return 'r:${timestamp}_$senderPrefix:$emoji';
|
||||
}
|
||||
|
||||
/// Extract sender prefix from public key hex (first 8 chars)
|
||||
static String getSenderPrefix(String senderKeyHex) {
|
||||
return senderKeyHex.substring(0, 8);
|
||||
return ReactionInfo(targetHash: match.group(1)!, emoji: emoji);
|
||||
}
|
||||
}
|
||||
|
||||
+15
-6
@@ -262,8 +262,9 @@ class Smaz {
|
||||
".com",
|
||||
];
|
||||
|
||||
static final List<Uint8List> _rcbBytes =
|
||||
_rcb.map((s) => Uint8List.fromList(ascii.encode(s))).toList(growable: false);
|
||||
static final List<Uint8List> _rcbBytes = _rcb
|
||||
.map((s) => Uint8List.fromList(ascii.encode(s)))
|
||||
.toList(growable: false);
|
||||
static final int _maxEntryLen = _rcbBytes.fold(0, (maxLen, entry) {
|
||||
return entry.length > maxLen ? entry.length : maxLen;
|
||||
});
|
||||
@@ -358,24 +359,32 @@ class Smaz {
|
||||
final code = input[index];
|
||||
if (code == _verbatimSingle) {
|
||||
if (index + 1 >= input.length) {
|
||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim byte.');
|
||||
throw const FormatException(
|
||||
'Invalid SMAZ stream: truncated verbatim byte.',
|
||||
);
|
||||
}
|
||||
out.addByte(input[index + 1]);
|
||||
index += 2;
|
||||
} else if (code == _verbatimRun) {
|
||||
if (index + 1 >= input.length) {
|
||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim length.');
|
||||
throw const FormatException(
|
||||
'Invalid SMAZ stream: truncated verbatim length.',
|
||||
);
|
||||
}
|
||||
final len = input[index + 1] + 1;
|
||||
final end = index + 2 + len;
|
||||
if (end > input.length) {
|
||||
throw const FormatException('Invalid SMAZ stream: truncated verbatim run.');
|
||||
throw const FormatException(
|
||||
'Invalid SMAZ stream: truncated verbatim run.',
|
||||
);
|
||||
}
|
||||
out.add(input.sublist(index + 2, end));
|
||||
index = end;
|
||||
} else {
|
||||
if (code >= _rcbBytes.length) {
|
||||
throw const FormatException('Invalid SMAZ stream: code out of range.');
|
||||
throw const FormatException(
|
||||
'Invalid SMAZ stream: code out of range.',
|
||||
);
|
||||
}
|
||||
out.add(_rcbBytes[code]);
|
||||
index += 1;
|
||||
|
||||
@@ -8,7 +8,10 @@ class Utf8LengthLimitingTextInputFormatter extends TextInputFormatter {
|
||||
const Utf8LengthLimitingTextInputFormatter(this.maxBytes);
|
||||
|
||||
@override
|
||||
TextEditingValue formatEditUpdate(TextEditingValue oldValue, TextEditingValue newValue) {
|
||||
TextEditingValue formatEditUpdate(
|
||||
TextEditingValue oldValue,
|
||||
TextEditingValue newValue,
|
||||
) {
|
||||
if (maxBytes <= 0) return oldValue;
|
||||
final bytes = utf8.encode(newValue.text);
|
||||
if (bytes.length <= maxBytes) return newValue;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Неуспешно изтриване на канала \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "bg",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Контакти",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Брой контакти",
|
||||
"settings_infoChannelCount": "Брой канали",
|
||||
"settings_presets": "Предварителни настройки",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Честота (MHz)",
|
||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||
"settings_frequencyInvalid": "Невалидна честота (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Мощност (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Невалидна мощност на TX (0-22 dBm)",
|
||||
"settings_longRange": "Дълъг обхват",
|
||||
"settings_fastSpeed": "Бърза скорост",
|
||||
"settings_error": "Грешка: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Публичен канал",
|
||||
"channels_privateChannel": "Частен канал",
|
||||
"channels_editChannel": "Редактирай канал",
|
||||
"channels_muteChannel": "Заглуши канала",
|
||||
"channels_unmuteChannel": "Включи известията на канала",
|
||||
"channels_deleteChannel": "Изтрий канала",
|
||||
"channels_deleteChannelConfirm": "Изтрий \"{name}\"? Това не може да бъде отменено.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Отваряне на връзката?",
|
||||
"chat_openLinkConfirmation": "Искате ли да отворите тази връзка в браузъра си?",
|
||||
"chat_open": "Отвори",
|
||||
"chat_couldNotOpenLink": "Не можа да се отвори връзката: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Невалиден формат на връзката",
|
||||
"map_title": "Карта на възлите",
|
||||
"map_noNodesWithLocation": "Няма възли с данни за местоположение.",
|
||||
"map_nodesNeedGps": "Възлагат се възлозите да споделят техните GPS координати,\nза да се появят на картата.",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||
"repeater_neighbours": "Съседи",
|
||||
"repeater_neighborsSubtitle": "Преглед на съседни възли с нулев скок.",
|
||||
"repeater_neighbors": "Съседи",
|
||||
"neighbors_receivedData": "Получени данни за съседи",
|
||||
"neighbors_requestTimedOut": "Съседите поискат изтичане на време.",
|
||||
"neighbors_errorLoading": "Грешка при зареждане на съседи: {error}",
|
||||
"neighbors_repeatersNeighbours": "Повторители Съседи",
|
||||
"neighbors_repeatersNeighbors": "Повторители Съседи",
|
||||
"neighbors_noData": "Няма налични данни за съседи.",
|
||||
"channels_createPrivateChannel": "Създай Частен Канал",
|
||||
"channels_joinPrivateChannel": "Присъедини се към Частен Канал",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Това ще изтрие също {count} канал(а) и техните съобщения.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Остави общността \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Общ хаштаг (всеки може да се присъедини)",
|
||||
"community_communityHashtag": "Общностен хаштаг",
|
||||
"community_communityHashtagDesc": "Само за членове на общността",
|
||||
"community_forCommunity": "За {name}"
|
||||
"community_forCommunity": "За {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecretConfirm": "Регенерация на секретния ключ за \"{name}\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.",
|
||||
"community_secretRegenerated": "Секретно презареждане за \"{name}\"",
|
||||
"community_regenerateSecret": "Регенерейрай секрет",
|
||||
"community_regenerate": "Регенерация",
|
||||
"community_updateSecret": "Актуализирай тайна",
|
||||
"community_scanToUpdateSecret": "Сканьорвайте новия QR код, за да актуализирате секрета за \"{name}\"",
|
||||
"community_secretUpdated": "Секретно обновено за \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Вие",
|
||||
"pathTrace_notAvailable": "Пътека за проследяване не е достъпна.",
|
||||
"contacts_pathTrace": "Пътен проследяване",
|
||||
"pathTrace_refreshTooltip": "Обнови Path Trace.",
|
||||
"pathTrace_failed": "Пътят за проследяване не успя.",
|
||||
"contacts_repeaterPing": "Пингване на повторителя",
|
||||
"contacts_repeaterPathTrace": "Трасировка до повторител",
|
||||
"contacts_ping": "Пинг",
|
||||
"contacts_chatTraceRoute": "Трасиране на път",
|
||||
"contacts_roomPathTrace": "Трасиране на път до съ",
|
||||
"contacts_roomPing": "Ping на сървъра на стаята",
|
||||
"contacts_pathTraceTo": "Проследи маршрут към {name}",
|
||||
"appSettings_languageUk": "Украински",
|
||||
"contacts_clipboardEmpty": "Клипборда е празна.",
|
||||
"contacts_invalidAdvertFormat": "Невалидни данни за контакт",
|
||||
"appSettings_languageRu": "Руски",
|
||||
"appSettings_enableMessageTracing": "Разрешаване на проследяване на съобщения",
|
||||
"appSettings_enableMessageTracingSubtitle": "Показване на подробни метаданни за маршрутизация и синхронизация за съобщения",
|
||||
"contacts_contactImported": "Контактът е импортиран.",
|
||||
"contacts_zeroHopAdvert": "Реклама без скок",
|
||||
"contacts_contactImportFailed": "Контактът не е успешно импортиран.",
|
||||
"contacts_floodAdvert": "Потопна реклама",
|
||||
"contacts_addContactFromClipboard": "Добави контакт от клипборда",
|
||||
"contacts_copyAdvertToClipboard": "Копирай обявата в клипборда",
|
||||
"contacts_ShareContact": "Копирай контакт в клипборда",
|
||||
"contacts_ShareContactZeroHop": "Сподели контакт чрез обява",
|
||||
"contacts_contactAdvertCopied": "Рекламата е копирана в клипборда.",
|
||||
"contacts_zeroHopContactAdvertFailed": "Неуспешно изпращане на контакт.",
|
||||
"contacts_zeroHopContactAdvertSent": "Изпратен контакт по обява.",
|
||||
"contacts_contactAdvertCopyFailed": "Копирането на обявата в клипборда не успя.",
|
||||
"notification_activityTitle": "Активност на MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{съобщение} other{съобщения}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{съобщение в канал} other{съобщения в канали}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{нов възел} other{нови възли}}",
|
||||
"notification_newTypeDiscovered": "Открит нов {contactType}",
|
||||
"notification_receivedNewMessage": "Получено ново съобщение",
|
||||
"settings_gpxExportContactsSubtitle": "Експортира спътници с местоположение в GPX файл.",
|
||||
"settings_gpxExportRepeatersSubtitle": "Изпраща повторители / roomserver с местоположение в GPX файл.",
|
||||
"settings_gpxExportAll": "Експортирай всички контакти в GPX",
|
||||
"settings_gpxExportAllSubtitle": "Експортира всички контакти с местоположение в файл GPX.",
|
||||
"settings_gpxExportRepeaters": "Експортиране на повтарящи се устройства / сървър на стаята до GPX",
|
||||
"settings_gpxExportContacts": "Експортирай спътници към GPX",
|
||||
"settings_gpxExportSuccess": "Успешно изlexport на файл GPX.",
|
||||
"settings_gpxExportNoContacts": "Няма контакти за изlexport.",
|
||||
"settings_gpxExportChat": "Местоположения на спътници",
|
||||
"settings_gpxExportError": "Възникна грешка при изнасяне.",
|
||||
"settings_gpxExportRepeatersRoom": "Местоположения на повторител и сървър на стаята",
|
||||
"settings_gpxExportNotAvailable": "Не е поддържан на вашето устройство/ОС",
|
||||
"settings_gpxExportAllContacts": "Местоположения на всички контакти",
|
||||
"settings_gpxExportShareText": "Картинни данни изнесени от meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open износ на данни за карта в формат GPX",
|
||||
"pathTrace_someHopsNoLocation": "Един или повече от хмелите липсва местоположение!",
|
||||
"map_pathTraceCancelled": "Отменен е следването на пътя.",
|
||||
"pathTrace_clearTooltip": "Изчисти пътя",
|
||||
"map_removeLast": "Премахни Последно",
|
||||
"map_runTrace": "Изпълни Път на Следване",
|
||||
"map_tapToAdd": "Натиснете върху възлите, за да ги добавите към пътя.",
|
||||
"scanner_bluetoothOff": "Bluetooth е изключен.",
|
||||
"scanner_enableBluetooth": "Активирайте Bluetooth",
|
||||
"scanner_bluetoothOffMessage": "Моля, активирайте Bluetooth, за да сканирате за устройства.",
|
||||
"scanner_chromeRequired": "Изисква се браузър Chrome",
|
||||
"scanner_chromeRequiredMessage": "Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.",
|
||||
"snrIndicator_lastSeen": "Последно видян",
|
||||
"snrIndicator_nearByRepeaters": "Близки повтарящи се устройства",
|
||||
"chat_ShowAllPaths": "Покажи всички пътища",
|
||||
"settings_clientRepeatSubtitle": "Позволете на това устройство да предава пакети към мрежата за други устройства.",
|
||||
"settings_clientRepeatFreqWarning": "За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.",
|
||||
"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 връзка."
|
||||
}
|
||||
|
||||
+449
-46
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanal {name} konnte nicht gelöscht werden",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "de",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakte",
|
||||
@@ -74,7 +82,7 @@
|
||||
"settings_title": "Einstellungen",
|
||||
"settings_deviceInfo": "Geräteinformationen",
|
||||
"settings_appSettings": "App-Einstellungen",
|
||||
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmungen",
|
||||
"settings_appSettingsSubtitle": "Benachrichtigungen, Messaging und Kartenwahrnehmung",
|
||||
"settings_nodeSettings": "Knoten-Einstellungen",
|
||||
"settings_nodeName": "Knotenname",
|
||||
"settings_nodeNameNotSet": "Nicht festgelegt",
|
||||
@@ -96,14 +104,14 @@
|
||||
"settings_privacyModeEnabled": "Datenschutzmodus aktiviert",
|
||||
"settings_privacyModeDisabled": "Datenschutzmodus deaktiviert",
|
||||
"settings_actions": "Aktionen",
|
||||
"settings_sendAdvertisement": "Sende eine Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende Ankündigung",
|
||||
"settings_sendAdvertisement": "Sende Ankündigung",
|
||||
"settings_sendAdvertisementSubtitle": "Sende eine Ankündigung",
|
||||
"settings_advertisementSent": "Ankündigung gesendet",
|
||||
"settings_syncTime": "Zeitsynchronisierung",
|
||||
"settings_syncTimeSubtitle": "Stelle die Gerätezeit auf die Uhrzeit des Telefons ein",
|
||||
"settings_timeSynchronized": "Zeit synchronisiert",
|
||||
"settings_refreshContacts": "Kontakte aktualisieren",
|
||||
"settings_refreshContactsSubtitle": "Kontakte-Liste vom Gerät neu laden",
|
||||
"settings_refreshContactsSubtitle": "Kontakt-Liste vom Gerät neu laden",
|
||||
"settings_rebootDevice": "Gerät neu starten",
|
||||
"settings_rebootDeviceSubtitle": "MeshCore-Gerät neu starten",
|
||||
"settings_rebootDeviceConfirm": "Sind Sie sicher, dass Sie das Gerät neu starten möchten? Sie werden getrennt.",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Anzahl Kontakte",
|
||||
"settings_infoChannelCount": "Anzahl Kanäle",
|
||||
"settings_presets": "Voreinstellungen",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequenz (MHz)",
|
||||
"settings_frequencyHelper": "300,00 - 2.500,00",
|
||||
"settings_frequencyInvalid": "Ungültige Frequenz (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX-Leistung (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ungültige TX-Leistung (0-22 dBm)",
|
||||
"settings_longRange": "Grosse Reichweite",
|
||||
"settings_fastSpeed": "Schnelle Geschwindigkeit",
|
||||
"settings_error": "Fehler: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -266,7 +269,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_manageRepeater": "Wiederholungen verwalten",
|
||||
"contacts_manageRepeater": "Repeater verwalten",
|
||||
"contacts_roomLogin": "Raum-Login",
|
||||
"contacts_openChat": "Öffne Chat",
|
||||
"contacts_editGroup": "Gruppe bearbeiten",
|
||||
@@ -293,8 +296,8 @@
|
||||
"contacts_filterContacts": "Filtert Kontakte...",
|
||||
"contacts_noContactsMatchFilter": "Keine Kontakte passen zu Ihrem Filter",
|
||||
"contacts_noMembers": "Keine Mitglieder",
|
||||
"contacts_lastSeenNow": "gerade gesehen",
|
||||
"contacts_lastSeenMinsAgo": "Letzte Sichtung vor {minutes} Minuten.",
|
||||
"contacts_lastSeenNow": "kürzlich",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} Min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -302,8 +305,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Letzte Sichtung vor 1 Stunde.",
|
||||
"contacts_lastSeenHoursAgo": "Letzte Sichtung vor {hours} Stunden.",
|
||||
"contacts_lastSeenHourAgo": "~ 1 Std.",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} Std.",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -311,8 +314,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Letzte Sichtung vor 1 Tag",
|
||||
"contacts_lastSeenDaysAgo": "Letzte Sichtung {days} Tage zuvor",
|
||||
"contacts_lastSeenDayAgo": "~ 1 Tag",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} Tage",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Öffentlicher Kanal",
|
||||
"channels_privateChannel": "Privater Kanal",
|
||||
"channels_editChannel": "Kanal bearbeiten",
|
||||
"channels_muteChannel": "Kanal stummschalten",
|
||||
"channels_unmuteChannel": "Kanal Stummschaltung aufheben",
|
||||
"channels_deleteChannel": "Lösche den Kanal",
|
||||
"channels_deleteChannelConfirm": "Löschen von \"{name}\"? Dies kann nicht rückgängig gemacht werden.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -360,7 +365,7 @@
|
||||
"channels_channelIndexLabel": "Kanalindex",
|
||||
"channels_channelName": "Kanalname",
|
||||
"channels_usePublicChannel": "Verwende öffentlichen Kanal",
|
||||
"channels_standardPublicPsk": "Standard-Öffentliche PSK",
|
||||
"channels_standardPublicPsk": "Öffentliche Standard PSK",
|
||||
"channels_pskHex": "PSK (Hex)",
|
||||
"channels_generateRandomPsk": "Zufällige PSK generieren",
|
||||
"channels_enterChannelName": "Bitte geben Sie einen Kanalnamen ein.",
|
||||
@@ -489,8 +494,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"debugFrame_textMessageHeader": "Textnachricht-Frame:",
|
||||
"debugFrame_destinationPubKey": "- Ziel-Pub-Schlüssel: {pubKey}",
|
||||
"debugFrame_textMessageHeader": "Textnachrichten Frame:",
|
||||
"debugFrame_destinationPubKey": "- Ziel-Public-Schlüssel: {pubKey}",
|
||||
"@debugFrame_destinationPubKey": {
|
||||
"placeholders": {
|
||||
"pubKey": {
|
||||
@@ -540,7 +545,7 @@
|
||||
"chat_routingMode": "Routenmodus",
|
||||
"chat_autoUseSavedPath": "Automatisch (gespeicherten Pfad verwenden)",
|
||||
"chat_forceFloodMode": "Flut-Modus erzwingen",
|
||||
"chat_recentAckPaths": "Aktuelle ACK-Pfade (tasten, um zu verwenden):",
|
||||
"chat_recentAckPaths": "Aktuelle ACK-Pfade (antippen, um zu verwenden):",
|
||||
"chat_pathHistoryFull": "Die Pfadhistorie ist voll. Entferne Einträge, um neue hinzuzufügen.",
|
||||
"chat_hopSingular": "Sprung",
|
||||
"chat_hopPlural": "Sprünge",
|
||||
@@ -554,7 +559,7 @@
|
||||
},
|
||||
"chat_successes": "Erfolgreich",
|
||||
"chat_removePath": "Pfad entfernen",
|
||||
"chat_noPathHistoryYet": "Keine eine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
||||
"chat_noPathHistoryYet": "Keine Pfadhistorie vorhanden.\nSende eine Nachricht, um Pfade zu entdecken.",
|
||||
"chat_pathActions": "Pfadaktionen:",
|
||||
"chat_setCustomPath": "Lege benutzerdefinierten Pfad fest",
|
||||
"chat_setCustomPathSubtitle": "Manuellen Routenpfad festlegen",
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Link öffnen?",
|
||||
"chat_openLinkConfirmation": "Möchten Sie diesen Link in Ihrem Browser öffnen?",
|
||||
"chat_open": "Öffnen",
|
||||
"chat_couldNotOpenLink": "Link konnte nicht geöffnet werden: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Ungültiges Link-Format",
|
||||
"map_title": "Karte",
|
||||
"map_noNodesWithLocation": "Keine Knoten mit Standortdaten",
|
||||
"map_nodesNeedGps": "Knoten müssen ihre GPS-Koordinaten teilen,\num auf der Karte zu erscheinen.",
|
||||
@@ -705,7 +722,7 @@
|
||||
"mapCache_cacheArea": "Zwischenspeicherbereich",
|
||||
"mapCache_useCurrentView": "Aktuelle Ansicht verwenden",
|
||||
"mapCache_zoomRange": "Zoom Bereich",
|
||||
"mapCache_estimatedTiles": "Geschätzte Fliesen: {count}",
|
||||
"mapCache_estimatedTiles": "Geschätzte Kacheln: {count}",
|
||||
"@mapCache_estimatedTiles": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -842,7 +859,7 @@
|
||||
},
|
||||
"path_enterCustomPath": "Gebe Pfad ein",
|
||||
"path_currentPathLabel": "Aktueller Pfad",
|
||||
"path_hexPrefixInstructions": "Gebe für jeden Hopfen 2-stellige Hex-Präfixe ein, getrennt durch Kommas.",
|
||||
"path_hexPrefixInstructions": "Gebe für jeden Zwischen-Hop das 2-stellige Hex-Präfix ein, getrennt durch Kommas.",
|
||||
"path_hexPrefixExample": "Beispiel: A1,F2,3C (jeder Knoten verwendet den ersten Byte seines öffentlichen Schlüssels)",
|
||||
"path_labelHexPrefixes": "Pfad (Hex-Präfixe)",
|
||||
"path_helperMaxHops": "Max 64 Sprünge. Jede Präfixe ist 2 Hexadezimalzeichen (1 Byte)",
|
||||
@@ -875,7 +892,7 @@
|
||||
"repeater_forceFloodMode": "Flut-Modus erzwingen",
|
||||
"repeater_pathManagement": "Pfadverwaltung",
|
||||
"repeater_refresh": "Aktualisieren",
|
||||
"repeater_statusRequestTimeout": "Statusanfrage zeitweise fehlgeschlagen.",
|
||||
"repeater_statusRequestTimeout": "Statusanfrage durch Timeout fehlgeschlagen.",
|
||||
"repeater_errorLoadingStatus": "Fehler beim Laden des Status: {error}",
|
||||
"@repeater_errorLoadingStatus": {
|
||||
"placeholders": {
|
||||
@@ -945,7 +962,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_duplicatesFloodDirect": "Überflut: {flood}, Direkt: {direct}",
|
||||
"repeater_duplicatesFloodDirect": "Flut: {flood}, Direkt: {direct}",
|
||||
"@repeater_duplicatesFloodDirect": {
|
||||
"placeholders": {
|
||||
"flood": {
|
||||
@@ -971,7 +988,7 @@
|
||||
"repeater_adminPassword": "Admin-Passwort",
|
||||
"repeater_adminPasswordHelper": "Vollzugriffspasswort",
|
||||
"repeater_guestPassword": "Gast-Passwort",
|
||||
"repeater_guestPasswordHelper": "Schreibgeschützter Zugriffspasswort",
|
||||
"repeater_guestPasswordHelper": "Schreibgeschütztes Zugriffspasswort",
|
||||
"repeater_radioSettings": "Funk Einstellungen",
|
||||
"repeater_frequencyMhz": "Frequenz (MHz)",
|
||||
"repeater_frequencyHelper": "300-2500 MHz",
|
||||
@@ -1014,7 +1031,7 @@
|
||||
"repeater_encryptedAdvertInterval": "Intervall der verschlüsselten Ankündigung",
|
||||
"repeater_dangerZone": "Gefahrenzone",
|
||||
"repeater_rebootRepeater": "Neustart Repeater",
|
||||
"repeater_rebootRepeaterSubtitle": "Wiederholen Sie das Repeater-Gerät.",
|
||||
"repeater_rebootRepeaterSubtitle": "Repeater-Gerät neu starten.",
|
||||
"repeater_rebootRepeaterConfirm": "Sind Sie sicher, dass Sie diesen Repeater neu starten möchten?",
|
||||
"repeater_regenerateIdentityKey": "Schlüssel für die Identitätswiederherstellung",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Neuen öffentlichen/privaten Schlüsselpaar generieren",
|
||||
@@ -1074,11 +1091,11 @@
|
||||
}
|
||||
},
|
||||
"repeater_cliTitle": "Repeater CLI",
|
||||
"repeater_debugNextCommand": "Fehlersuche Nächster Befehl",
|
||||
"repeater_debugNextCommand": "Fehlersuche des nächsten Befehls",
|
||||
"repeater_commandHelp": "Hilfe",
|
||||
"repeater_clearHistory": "Löschen der Historie",
|
||||
"repeater_noCommandsSent": "Noch keine Befehle gesendet.",
|
||||
"repeater_typeCommandOrUseQuick": "Geben Sie einen Befehl unten ein oder verwenden Sie Schnellbefehle",
|
||||
"repeater_typeCommandOrUseQuick": "Geben Sie unten einen Befehl ein oder verwenden Sie die Schnellbefehle",
|
||||
"repeater_enterCommandHint": "Geben Sie den Befehl ein...",
|
||||
"repeater_previousCommand": "Vorhergehende Aktion",
|
||||
"repeater_nextCommand": "Nächste Aktion",
|
||||
@@ -1120,7 +1137,7 @@
|
||||
"repeater_cliHelpSetLat": "Legt die Breitengrad der Ankündigung fest. (dezimale Grad)",
|
||||
"repeater_cliHelpSetLon": "Legt die Längengrade der Ankündigung fest. (dezimale Grad)",
|
||||
"repeater_cliHelpSetRadio": "Legt komplett neue Radio-Parameter fest und speichert diese als Präferenzen. Benötigt einen \"Reboot\"-Befehl, um sie anzuwenden.",
|
||||
"repeater_cliHelpSetRxDelay": "Sets (experimentell) als Basis (muss > 1 sein für den Effekt) zur Anwendung einer leichten Verzögerung bei empfangenen Paketen, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
||||
"repeater_cliHelpSetRxDelay": "Fügt eine leichte Verzögerung bei empfangenen Paketen hinzu, basierend auf Signalstärke/Punktzahl. Auf 0 setzen, um die Funktion zu deaktivieren.",
|
||||
"repeater_cliHelpSetTxDelay": "Legt einen Faktor fest, der mit der Zeit bei voller Zuluft für ein Flood-Mode-Paket und mit einem zufälligen Slot-System multipliziert wird, um dessen Weiterleitung zu verzögern (um Kollisionen zu vermeiden).",
|
||||
"repeater_cliHelpSetDirectTxDelay": "Ähnlich wie txdelay, aber zum Anwenden einer zufälligen Verzögerung bei der Weiterleitung von Direktmodus-Paketen.",
|
||||
"repeater_cliHelpSetBridgeEnabled": "Brücke aktivieren/deaktivieren.",
|
||||
@@ -1131,14 +1148,14 @@
|
||||
"repeater_cliHelpSetAdcMultiplier": "Legt einen benutzerdefinierten Faktor zur Anpassung der gemeldeten Batteriewirkspannung fest (nur auf ausgewählten Boards unterstützt).",
|
||||
"repeater_cliHelpTempRadio": "Legt vorübergehende Funkparameter für die angegebene Anzahl von Minuten fest und kehrt anschließend zu den ursprünglichen Funkparametern zurück (wird nicht in den Einstellungen gespeichert).",
|
||||
"repeater_cliHelpSetPerm": "Ändert die ACL. Entfernt das passende Eintragen (durch Pubkey-Präfix), wenn \"permissions\" auf 0 steht. Fügt ein neues Eintragen hinzu, wenn die Pubkey-Hex-Länge vollständig ist und nicht bereits in der ACL vorhanden ist. Aktualisiert das Eintragen anhand des übereinstimmenden Pubkey-Präfix. Berechtigungsbits variieren je nach Firmware-Rolle, aber die unteren 2 Bits sind: 0 (Gast), 1 (Nur Lesen), 2 (Lesen/Schreiben), 3 (Admin)",
|
||||
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp none, rs232, espnow ab.",
|
||||
"repeater_cliHelpGetBridgeType": "Ruft Brückentyp: none, rs232, espnow ab.",
|
||||
"repeater_cliHelpLogStart": "Beginnt die Paketprotokollierung in das Dateisystem.",
|
||||
"repeater_cliHelpLogStop": "Stoppt das Paketprotokollieren in das Dateisystem.",
|
||||
"repeater_cliHelpLogErase": "Löscht die Paketprotokolle aus dem Dateisystem.",
|
||||
"repeater_cliHelpNeighbors": "Zeigt eine Liste anderer Repeater-Knoten an, die über Zero-Hop-Ankündigung gehört wurden. Jede Zeile ist id-prefix-hex:timestamp:snr-times-4",
|
||||
"repeater_cliHelpNeighborRemove": "Entfernt das erste übereinstimmende Element (über Pubkey-Präfix (hex)) aus der Liste der Nachbarn.",
|
||||
"repeater_cliHelpRegion": "Listet alle definierten Regionen auf.",
|
||||
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingedruckt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile/des Befehls.",
|
||||
"repeater_cliHelpRegionLoad": "Hinweis: Dies ist ein spezieller Mehrbefehl-Aufruf. Jeder nachfolgende Befehl ist ein Regionsname (eingerückt mit Leerzeichen zur Angabe der übergeordneten Hierarchie, mit mindestens einem Leerzeichen). Beendet durch das Senden einer Leerzeile.",
|
||||
"repeater_cliHelpRegionGet": "Sucht die Region mit dem gegebenen Namenspräfix (oder \"\\\" für den globalen Scope) und antwortet mit \"-> region-name (parent-name) 'F'\".",
|
||||
"repeater_cliHelpRegionPut": "Fügt eine Region-Definition mit dem angegebenen Namen hinzu oder aktualisiert diese.",
|
||||
"repeater_cliHelpRegionRemove": "Löscht eine Regiondefinition mit dem angegebenen Namen. (muss genau übereinstimmen und keine Kindregionen haben)",
|
||||
@@ -1231,7 +1248,7 @@
|
||||
"channelPath_otherObservedPaths": "Sonstige beobachtete Pfade",
|
||||
"channelPath_repeaterHops": "Repeater-Sprünge",
|
||||
"channelPath_noHopDetails": "Die Detailangaben für dieses Paket sind nicht verfügbar.",
|
||||
"channelPath_messageDetails": "Nachrichtsdetails",
|
||||
"channelPath_messageDetails": "Nachrichtendetails",
|
||||
"channelPath_senderLabel": "Sender",
|
||||
"channelPath_timeLabel": "Zeit",
|
||||
"channelPath_repeatsLabel": "Wiederholungen",
|
||||
@@ -1332,10 +1349,13 @@
|
||||
"listFilter_az": "A-Z",
|
||||
"listFilter_filters": "Filtere",
|
||||
"listFilter_all": "Alle",
|
||||
"listFilter_favorites": "Favoriten",
|
||||
"listFilter_addToFavorites": "Zu Favoriten hinzufügen",
|
||||
"listFilter_removeFromFavorites": "Aus Favoriten entfernen",
|
||||
"listFilter_users": "Benutzer",
|
||||
"listFilter_repeaters": "Repeater",
|
||||
"listFilter_roomServers": "Raumserver",
|
||||
"listFilter_unreadOnly": "Nur nicht gelesen",
|
||||
"listFilter_unreadOnly": "Nicht gelesen",
|
||||
"listFilter_newGroup": "Neue Gruppe",
|
||||
"@neighbors_errorLoading": {
|
||||
"placeholders": {
|
||||
@@ -1344,13 +1364,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Nachbarn",
|
||||
"repeater_neighboursSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||
"neighbors_receivedData": "Empfangene Nachbarendaten",
|
||||
"neighbors_requestTimedOut": "Nachbarn melden zeitweise Ausfall.",
|
||||
"repeater_neighbors": "Nachbarn",
|
||||
"repeater_neighborsSubtitle": "Anzahl der Hop-Nachbarn anzeigen.",
|
||||
"neighbors_receivedData": "Empfangene Nachbarsdaten",
|
||||
"neighbors_requestTimedOut": "Anfrage durch Timeout fehlgeschlagen.",
|
||||
"neighbors_errorLoading": "Fehler beim Laden der Nachbarn: {error}",
|
||||
"neighbors_repeatersNeighbours": "Wiederholer Nachbarn",
|
||||
"neighbors_noData": "Keine Nachbardaten verfügbar.",
|
||||
"neighbors_repeatersNeighbors": "Nachbarn",
|
||||
"neighbors_noData": "Keine Nachbarsdaten verfügbar.",
|
||||
"channels_joinPrivateChannel": "Treten Sie einem privaten Kanal bei",
|
||||
"channels_joinPrivateChannelDesc": "Manuelle Eingabe eines geheimen Schlüssels.",
|
||||
"channels_createPrivateChannel": "Erstelle einen privaten Kanal",
|
||||
@@ -1377,8 +1397,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"neighbors_heardAgo": "Hörte: {time} vor her.",
|
||||
"neighbors_unknownContact": "Unbekannte {pubkey}",
|
||||
"neighbors_heardAgo": "Gehört vor: {time}",
|
||||
"neighbors_unknownContact": "Unbekannt {pubkey}",
|
||||
"settings_locationGPSEnable": "GPS aktivieren",
|
||||
"settings_locationGPSEnableSubtitle": "Aktiviert GPS zur automatischen Aktualisierung des Standorts.",
|
||||
"settings_locationIntervalSec": "Intervall für GPS (Sekunden)",
|
||||
@@ -1473,16 +1493,399 @@
|
||||
"community_deleteChannelsWarning": "Dies löscht auch {count} Kanal/Kanäle und deren Nachrichten.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Community \"{name}\" verlassen",
|
||||
"community_addHashtagChannel": "Füge einen Community-Hashtag hinzu",
|
||||
"community_addHashtagChannelDesc": "Füge einen Hashtag-Kanal für diese Community hinzu",
|
||||
"community_selectCommunity": "Wählen Sie Community",
|
||||
"community_selectCommunity": "Wählen Sie eine Community",
|
||||
"community_regularHashtag": "Regulärer Hashtag",
|
||||
"community_regularHashtagDesc": "Öffentliches Hashtag (jeder kann teilnehmen)",
|
||||
"community_regularHashtagDesc": "Öffentlicher Hashtag (jeder kann teilnehmen)",
|
||||
"community_communityHashtagDesc": "Nur für Mitglieder der Community",
|
||||
"community_forCommunity": "Für {name}",
|
||||
"community_communityHashtag": "Community Hashtag"
|
||||
"community_communityHashtag": "Community Hashtag",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerate": "Neu generieren",
|
||||
"community_secretRegenerated": "Wiederherstellung des Schlüssels für \"{name}\" erfolgreich",
|
||||
"community_regenerateSecretConfirm": "Nehmen Sie den geheimen Schlüssel für \"{name}\" neu auf? Alle Mitglieder müssen den neuen QR-Code scannen, um die Kommunikation fortzusetzen.",
|
||||
"community_regenerateSecret": "Neugenerierung des Schlüssels",
|
||||
"community_secretUpdated": "Schlüssel für \"{name}\" aktualisiert",
|
||||
"community_scanToUpdateSecret": "Scannen Sie den neuen QR-Code, um das Geheimnis für \"{name}\" zu aktualisieren.",
|
||||
"community_updateSecret": "Aktualisieren Sie den Schlüssel",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_refreshTooltip": "Path Trace aktualisieren.",
|
||||
"pathTrace_you": "Du",
|
||||
"pathTrace_failed": "Pfadverfolgung fehlgeschlagen.",
|
||||
"pathTrace_notAvailable": "Pfadverfolgung nicht verfügbar.",
|
||||
"contacts_pathTrace": "Pfadverfolgung",
|
||||
"contacts_ping": "Pingen",
|
||||
"contacts_repeaterPathTrace": "Pfadverfolgung zum Repeater",
|
||||
"contacts_repeaterPing": "Repeater pingen",
|
||||
"contacts_roomPathTrace": "Pfadverfolgung zum Raumserver",
|
||||
"contacts_roomPing": "Raumserver anpingen",
|
||||
"contacts_pathTraceTo": "Route nach {name} verfolgen",
|
||||
"contacts_chatTraceRoute": "Pfadverfolgungsroute",
|
||||
"appSettings_languageRu": "Russisch",
|
||||
"contacts_invalidAdvertFormat": "Ungültige Kontaktdaten",
|
||||
"contacts_clipboardEmpty": "Die Zwischenablage ist leer.",
|
||||
"appSettings_languageUk": "Ukrainisch",
|
||||
"appSettings_enableMessageTracing": "Nachrichtenverfolgung aktivieren",
|
||||
"appSettings_enableMessageTracingSubtitle": "Detaillierte Routing- und Timing-Metadaten für Nachrichten anzeigen",
|
||||
"contacts_contactImported": "Kontakt wurde importiert.",
|
||||
"contacts_contactImportFailed": "Kontakt konnte nicht importiert werden",
|
||||
"contacts_zeroHopAdvert": "Zero-Hop-Ankündigung",
|
||||
"contacts_floodAdvert": "Flut-Ankündigung",
|
||||
"contacts_addContactFromClipboard": "Kontakt aus Zwischenablage hinzufügen",
|
||||
"contacts_ShareContactZeroHop": "Kontakt über Anzeige teilen",
|
||||
"contacts_copyAdvertToClipboard": "Ankündigung in die Zwischenablage kopieren",
|
||||
"contacts_ShareContact": "Kontakt in die Zwischenablage kopieren",
|
||||
"contacts_zeroHopContactAdvertFailed": "Kontakt konnte nicht gesendet werden.",
|
||||
"contacts_zeroHopContactAdvertSent": "Kontakt über Anzeige gesendet",
|
||||
"contacts_contactAdvertCopied": "Anzeige in die Zwischenablage kopiert.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopieren der Ankündigung in die Zwischenablage fehlgeschlagen.",
|
||||
"notification_activityTitle": "MeshCore Aktivität",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{Nachricht} other{Nachrichten}}",
|
||||
"@notification_messagesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{Kanalnachricht} other{Kanalnachrichten}}",
|
||||
"@notification_channelMessagesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{neuer Knoten} other{neue Knoten}}",
|
||||
"@notification_newNodesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newTypeDiscovered": "Neuer {contactType} entdeckt",
|
||||
"@notification_newTypeDiscovered": {
|
||||
"placeholders": {
|
||||
"contactType": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_receivedNewMessage": "Neue Nachricht empfangen",
|
||||
"settings_gpxExportAll": "Alle Knoten als GPX exportieren",
|
||||
"settings_gpxExportAllSubtitle": "Exportiert alle Knoten mit einem Standort in eine GPX-Datei.",
|
||||
"settings_gpxExportRepeaters": "Repeater und Raumserver als GPX exportieren",
|
||||
"settings_gpxExportContacts": "Kontakte als GPX exportieren",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exportiert Repeater und Raumserver mit einem Standort in eine GPX-Datei.",
|
||||
"settings_gpxExportContactsSubtitle": "Exportiert Kontakte mit einem Ort in eine GPX-Datei.",
|
||||
"settings_gpxExportRepeatersRoom": "Repeater- und Raumserver-Standorte",
|
||||
"settings_gpxExportChat": "Kontaktstandorte",
|
||||
"settings_gpxExportNoContacts": "Keine Kontakte zum Exportieren.",
|
||||
"settings_gpxExportError": "Beim Export ist ein Fehler aufgetreten.",
|
||||
"settings_gpxExportNotAvailable": "Nicht auf Ihrem Gerät/Betriebssystem unterstützt",
|
||||
"settings_gpxExportSuccess": "GPX-Datei erfolgreich exportiert.",
|
||||
"settings_gpxExportAllContacts": "Alle Kontaktstandorte",
|
||||
"settings_gpxExportShareSubject": "GPX-Kartendaten aus meshcore-open exportieren",
|
||||
"settings_gpxExportShareText": "GPX-Kartendaten aus meshcore-open exportiert",
|
||||
"pathTrace_someHopsNoLocation": "Bei einer oder mehreren Knoten fehlt der Standort!",
|
||||
"map_removeLast": "Letztes Entfernen",
|
||||
"map_tapToAdd": "Tippen Sie auf Knoten, um sie zum Pfad hinzuzufügen.",
|
||||
"map_runTrace": "Pfadverlauf ausführen",
|
||||
"pathTrace_clearTooltip": "Pfad löschen",
|
||||
"map_pathTraceCancelled": "Pfadverfolgung abgebrochen.",
|
||||
"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_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_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_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."
|
||||
}
|
||||
|
||||
+769
-170
File diff suppressed because it is too large
Load Diff
+418
-15
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "No se pudo eliminar el canal \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "es",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contactos",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Número de contactos",
|
||||
"settings_infoChannelCount": "Número de canales",
|
||||
"settings_presets": "Preajustes",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frecuencia (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frecuencia inválida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potencia (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potencia de TX inválida (0-22 dBm)",
|
||||
"settings_longRange": "Largo Alcance",
|
||||
"settings_fastSpeed": "Velocidad Rápida",
|
||||
"settings_error": "Error: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -294,7 +297,7 @@
|
||||
"contacts_noContactsMatchFilter": "No hay contactos que coincidan con tu filtro",
|
||||
"contacts_noMembers": "No miembros",
|
||||
"contacts_lastSeenNow": "Última vez que se vio ahora",
|
||||
"contacts_lastSeenMinsAgo": "Última vez visto hace {minutes} minutos.",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -302,8 +305,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Última vez que se vio hace 1 hora",
|
||||
"contacts_lastSeenHoursAgo": "Última vez visto hace {hours} horas.",
|
||||
"contacts_lastSeenHourAgo": "~ 1 hora",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} horas",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -311,8 +314,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Última vez que se vio hace 1 día",
|
||||
"contacts_lastSeenDaysAgo": "Última vez visto hace {days} días.",
|
||||
"contacts_lastSeenDayAgo": "~ 1 día",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} días",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal público",
|
||||
"channels_privateChannel": "Canal privado",
|
||||
"channels_editChannel": "Editar canal",
|
||||
"channels_muteChannel": "Silenciar canal",
|
||||
"channels_unmuteChannel": "Activar canal",
|
||||
"channels_deleteChannel": "Eliminar canal",
|
||||
"channels_deleteChannelConfirm": "Eliminar \"{name}\"? Esto no se puede deshacer.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "¿Abrir enlace?",
|
||||
"chat_openLinkConfirmation": "¿Quiere abrir este enlace en su navegador?",
|
||||
"chat_open": "Abrir",
|
||||
"chat_couldNotOpenLink": "No se pudo abrir el enlace: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Formato de enlace no válido",
|
||||
"map_title": "Mapa de Nodos",
|
||||
"map_noNodesWithLocation": "No hay nodos con datos de ubicación",
|
||||
"map_nodesNeedGps": "Los nodos necesitan compartir sus coordenadas GPS\npara aparecer en el mapa",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vecinos",
|
||||
"repeater_neighboursSubtitle": "Ver vecinos de salto cero.",
|
||||
"repeater_neighbors": "Vecinos",
|
||||
"repeater_neighborsSubtitle": "Ver vecinos de salto cero.",
|
||||
"neighbors_receivedData": "Recibidas Datos de Vecinos",
|
||||
"neighbors_requestTimedOut": "Los vecinos solicitan que se desconecte.",
|
||||
"neighbors_errorLoading": "Error al cargar vecinos: {error}",
|
||||
"neighbors_repeatersNeighbours": "Repetidores Vecinos",
|
||||
"neighbors_repeatersNeighbors": "Repetidores Vecinos",
|
||||
"neighbors_noData": "No hay datos de vecinos disponibles.",
|
||||
"channels_joinPrivateChannel": "Únete a un Canal Privado",
|
||||
"channels_createPrivateChannel": "Crear un Canal Privado",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Esto también eliminará {count} canal(es) y sus mensajes.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Has salido de la comunidad \"{name}\"",
|
||||
@@ -1484,5 +1503,389 @@
|
||||
"community_regularHashtagDesc": "Hashtag público (cualquiera puede unirse)",
|
||||
"community_communityHashtag": "Hashtag de la Comunidad",
|
||||
"community_communityHashtagDesc": "Exclusivo para miembros de la comunidad",
|
||||
"community_forCommunity": "Para {name}"
|
||||
"community_forCommunity": "Para {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecret": "Regenerar Contraseña Secreta",
|
||||
"community_regenerateSecretConfirm": "Regenerar la clave secreta para \"{name}\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.",
|
||||
"community_secretRegenerated": "Código secreto regenerado para \"{name}\"",
|
||||
"community_regenerate": "Regenerar",
|
||||
"community_secretUpdated": "Confidencialidad actualizada para \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Escanear el nuevo código QR para actualizar el secreto de \"{name}\"",
|
||||
"community_updateSecret": "Actualizar Contraseña",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Tú",
|
||||
"pathTrace_failed": "El trazado de ruta falló.",
|
||||
"pathTrace_refreshTooltip": "Actualizar Path Trace",
|
||||
"contacts_pathTrace": "Rastreo de caminos",
|
||||
"contacts_repeaterPathTrace": "Rastrear ruta al repetidor",
|
||||
"contacts_repeaterPing": "Pingar repetidor",
|
||||
"contacts_ping": "Ping",
|
||||
"pathTrace_notAvailable": "El trazado de ruta no está disponible.",
|
||||
"contacts_roomPing": "Pingar servidor de sala",
|
||||
"contacts_roomPathTrace": "Rastreo de ruta al servidor de la habitación",
|
||||
"contacts_pathTraceTo": "Rastrear ruta a {name}",
|
||||
"contacts_chatTraceRoute": "Ruta de trazado",
|
||||
"appSettings_languageUk": "Ucraniano",
|
||||
"contacts_clipboardEmpty": "El portapapeles está vacío.",
|
||||
"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_floodAdvert": "Anuncio de inundación",
|
||||
"contacts_contactImported": "El contacto ha sido importado.",
|
||||
"contacts_contactImportFailed": "Contacto no se importó correctamente.",
|
||||
"contacts_zeroHopAdvert": "Anuncio de Zero Hop",
|
||||
"contacts_ShareContactZeroHop": "Compartir contacto por anuncio",
|
||||
"contacts_ShareContact": "Copiar contacto al Portapapeles",
|
||||
"contacts_copyAdvertToClipboard": "Copiar anuncio al portapapeles",
|
||||
"contacts_addContactFromClipboard": "Agregar contacto desde el portapapeles",
|
||||
"contacts_zeroHopContactAdvertFailed": "No se pudo enviar el contacto.",
|
||||
"contacts_zeroHopContactAdvertSent": "Envió contacto por anuncio.",
|
||||
"contacts_contactAdvertCopied": "Anuncio copiado al Portapapeles.",
|
||||
"contacts_contactAdvertCopyFailed": "Copiar anuncio al Portapapeles ha fallado.",
|
||||
"notification_activityTitle": "Actividad de MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{mensaje} other{mensajes}}",
|
||||
"@notification_messagesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{mensaje de canal} other{mensajes de canal}}",
|
||||
"@notification_channelMessagesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nuevo nodo} other{nuevos nodos}}",
|
||||
"@notification_newNodesCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_newTypeDiscovered": "Nuevo {contactType} descubierto",
|
||||
"@notification_newTypeDiscovered": {
|
||||
"placeholders": {
|
||||
"contactType": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"notification_receivedNewMessage": "Nuevo mensaje recibido",
|
||||
"settings_gpxExportContactsSubtitle": "Exporta compañeros con una ubicación a archivo GPX.",
|
||||
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala a GPX",
|
||||
"settings_gpxExportSuccess": "Archivo GPX exportado con éxito.",
|
||||
"settings_gpxExportNoContacts": "No hay contactos para exportar.",
|
||||
"settings_gpxExportNotAvailable": "No compatible con tu dispositivo/SO",
|
||||
"settings_gpxExportError": "Hubo un error al exportar.",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores o roomserver con una ubicación a un archivo GPX.",
|
||||
"settings_gpxExportAllSubtitle": "Exporta todos los contactos con una ubicación a un archivo GPX.",
|
||||
"settings_gpxExportAll": "Exportar todos los contactos a GPX",
|
||||
"settings_gpxExportContacts": "Exportar compañeros a GPX",
|
||||
"settings_gpxExportChat": "Ubicaciones de compañero",
|
||||
"settings_gpxExportRepeatersRoom": "Ubicaciones del servidor de repetidor y sala",
|
||||
"settings_gpxExportAllContacts": "Todas las ubicaciones de contactos",
|
||||
"settings_gpxExportShareText": "Datos del mapa exportados desde meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exportación de datos de mapa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Uno o más de los lúpulos carecen de una ubicación",
|
||||
"pathTrace_clearTooltip": "Borrar ruta",
|
||||
"map_runTrace": "Ejecutar Rastreo de Ruta",
|
||||
"map_tapToAdd": "Pulse en los nodos para agregarlos al camino.",
|
||||
"map_removeLast": "Eliminar último",
|
||||
"map_pathTraceCancelled": "Rastreo de ruta cancelado.",
|
||||
"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_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_clientRepeat": "Repetir sin conexión",
|
||||
"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."
|
||||
}
|
||||
|
||||
+446
-71
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Échec de la suppression de la chaîne \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "fr",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contacts",
|
||||
@@ -104,7 +112,7 @@
|
||||
"settings_timeSynchronized": "Synchronisation temporelle",
|
||||
"settings_refreshContacts": "Rafraîchir les Contacts",
|
||||
"settings_refreshContactsSubtitle": "Recharger la liste des contacts depuis l'appareil",
|
||||
"settings_rebootDevice": "Réinitialiser l'appareil",
|
||||
"settings_rebootDevice": "Redémarrer l'appareil",
|
||||
"settings_rebootDeviceSubtitle": "Redémarrer l'appareil MeshCore",
|
||||
"settings_rebootDeviceConfirm": "Êtes-vous sûr de vouloir redémarrer l'appareil ? Vous serez déconnecté.",
|
||||
"settings_debug": "Déboguer",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Nombre de contacts",
|
||||
"settings_infoChannelCount": "Nombre de canaux",
|
||||
"settings_presets": "Préréglages",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Fréquence (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2 500,0",
|
||||
"settings_frequencyInvalid": "Fréquence invalide (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Puissance (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Puissance TX invalide (0-22 dBm)",
|
||||
"settings_longRange": "Portée Longue",
|
||||
"settings_fastSpeed": "Vitesse Rapide",
|
||||
"settings_error": "Erreur : {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -210,8 +213,8 @@
|
||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6-3,65V)",
|
||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||
"appSettings_mapDisplay": "Affichage de la carte",
|
||||
"appSettings_showRepeaters": "Afficher les répétiteurs",
|
||||
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répétiteurs sur la carte",
|
||||
"appSettings_showRepeaters": "Afficher les répéteurs",
|
||||
"appSettings_showRepeatersSubtitle": "Afficher les nœuds répéteurs sur la carte",
|
||||
"appSettings_showChatNodes": "Afficher les nœuds de discussion",
|
||||
"appSettings_showChatNodesSubtitle": "Afficher les nœuds de chat sur la carte",
|
||||
"appSettings_showOtherNodes": "Afficher d'autres nœuds",
|
||||
@@ -266,8 +269,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_manageRepeater": "Gérer le répétiteur",
|
||||
"contacts_roomLogin": "Connexion Salle",
|
||||
"contacts_manageRepeater": "Gérer le répéteur",
|
||||
"contacts_roomLogin": "Connexion Room Server",
|
||||
"contacts_openChat": "Ouverture du Chat",
|
||||
"contacts_editGroup": "Modifier le groupe",
|
||||
"contacts_deleteGroup": "Supprimer le groupe",
|
||||
@@ -279,7 +282,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_newGroup": "Nouvelle Groupe",
|
||||
"contacts_newGroup": "Nouveau Groupe",
|
||||
"contacts_groupName": "Nom du groupe",
|
||||
"contacts_groupNameRequired": "Le nom du groupe est obligatoire.",
|
||||
"contacts_groupAlreadyExists": "Le groupe \"{name}\" existe déjà.",
|
||||
@@ -293,8 +296,8 @@
|
||||
"contacts_filterContacts": "Filtrer les contacts...",
|
||||
"contacts_noContactsMatchFilter": "Aucun contact ne correspond à votre filtre.",
|
||||
"contacts_noMembers": "Aucun membre",
|
||||
"contacts_lastSeenNow": "Dernière fois vu maintenant",
|
||||
"contacts_lastSeenMinsAgo": "Dernière fois vu il y a {minutes} minutes.",
|
||||
"contacts_lastSeenNow": "Vu maintenant",
|
||||
"contacts_lastSeenMinsAgo": "~ {minutes} min.",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -302,8 +305,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Dernière fois vu il y a 1 heure.",
|
||||
"contacts_lastSeenHoursAgo": "Dernière fois vu il y a {hours} heures.",
|
||||
"contacts_lastSeenHourAgo": "~ 1 heure",
|
||||
"contacts_lastSeenHoursAgo": "~ {hours} heures",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -311,8 +314,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Dernière fois vu il y a 1 jour",
|
||||
"contacts_lastSeenDaysAgo": "Dernière activité il y a {days} jours",
|
||||
"contacts_lastSeenDayAgo": "~ 1 jour",
|
||||
"contacts_lastSeenDaysAgo": "~ {days} jours",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal public",
|
||||
"channels_privateChannel": "Canal privé",
|
||||
"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_deleteChannelConfirm": "Supprimer {name}? Cela ne peut pas être annulé.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -394,7 +399,7 @@
|
||||
"channels_sortBy": "Trier par",
|
||||
"channels_sortManual": "Manuel",
|
||||
"channels_sortAZ": "A à Z",
|
||||
"channels_sortLatestMessages": "Dernières messages",
|
||||
"channels_sortLatestMessages": "Derniers messages",
|
||||
"channels_sortUnread": "Non lu",
|
||||
"chat_noMessages": "Aucun message pour le moment.",
|
||||
"chat_sendMessageToStart": "Envoyer un message pour commencer",
|
||||
@@ -436,7 +441,7 @@
|
||||
"chat_messageCopied": "Message copié",
|
||||
"chat_messageDeleted": "Message supprimé",
|
||||
"chat_retryingMessage": "Tentative de récupération.",
|
||||
"chat_retryCount": "Réessayer {current}/{max}",
|
||||
"chat_retryCount": "Essai {current}/{max}",
|
||||
"@chat_retryCount": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
@@ -542,9 +547,9 @@
|
||||
"chat_forceFloodMode": "Mode tout le réseau forcé",
|
||||
"chat_recentAckPaths": "Chemins ACK récents (touchez pour utiliser) :",
|
||||
"chat_pathHistoryFull": "L'historique du chemin est plein. Supprimez les entrées pour en ajouter de nouvelles.",
|
||||
"chat_hopSingular": "Sautez",
|
||||
"chat_hopPlural": "sautez",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"chat_hopSingular": "saut",
|
||||
"chat_hopPlural": "sauts",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{saut} other{sauts}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Ouvrir le lien ?",
|
||||
"chat_openLinkConfirmation": "Voulez-vous ouvrir ce lien dans votre navigateur ?",
|
||||
"chat_open": "Ouvrir",
|
||||
"chat_couldNotOpenLink": "Impossible d'ouvrir le lien : {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Format de lien invalide",
|
||||
"map_title": "Carte des nœuds",
|
||||
"map_noNodesWithLocation": "Aucun nœud avec des données de localisation",
|
||||
"map_nodesNeedGps": "Les nœuds doivent partager leurs coordonnées GPS\npour apparaître sur la carte.",
|
||||
@@ -624,7 +641,7 @@
|
||||
}
|
||||
},
|
||||
"map_chat": "Chat",
|
||||
"map_repeater": "Répétiteur",
|
||||
"map_repeater": "Répéteur",
|
||||
"map_room": "Salle",
|
||||
"map_sensor": "Capteur",
|
||||
"map_pinDm": "Clé (DM)",
|
||||
@@ -665,7 +682,7 @@
|
||||
"map_lastSeenTime": "Dernière fois vu",
|
||||
"map_sharedPin": "Clé partagée",
|
||||
"map_joinRoom": "Rejoindre la salle",
|
||||
"map_manageRepeater": "Gérer le répétiteur",
|
||||
"map_manageRepeater": "Gérer le répéteur",
|
||||
"mapCache_title": "Cache de Carte Hors Ligne",
|
||||
"mapCache_selectAreaFirst": "Sélectionner une zone pour la mise en cache en premier",
|
||||
"mapCache_noTilesToDownload": "Aucun tuilage à télécharger pour cette zone.",
|
||||
@@ -687,7 +704,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesWithFailed": "Tiles mis en cache ({downloaded}) ({failed} ratés)",
|
||||
"mapCache_cachedTilesWithFailed": "Tuiles mis en cache ({downloaded}) ({failed} ratés)",
|
||||
"@mapCache_cachedTilesWithFailed": {
|
||||
"placeholders": {
|
||||
"downloaded": {
|
||||
@@ -734,7 +751,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
"north": {
|
||||
@@ -751,7 +768,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"time_justNow": "Il y a tout juste maintenant",
|
||||
"time_justNow": "Maintenant",
|
||||
"time_minutesAgo": "{minutes} minutes auparavant",
|
||||
"@time_minutesAgo": {
|
||||
"placeholders": {
|
||||
@@ -788,13 +805,13 @@
|
||||
"time_allTime": "Tout le temps",
|
||||
"dialog_disconnect": "Déconnecter",
|
||||
"dialog_disconnectConfirm": "Êtes-vous sûr de vouloir vous déconnecter de cet appareil ?",
|
||||
"login_repeaterLogin": "Connexion au répétiteur",
|
||||
"login_roomLogin": "Connexion Salle",
|
||||
"login_repeaterLogin": "Connexion au répéteur",
|
||||
"login_roomLogin": "Connexion Room Server",
|
||||
"login_password": "Mot de passe",
|
||||
"login_enterPassword": "Entrez votre mot de passe",
|
||||
"login_savePassword": "Sauvegarder le mot de passe",
|
||||
"login_savePasswordSubtitle": "Le mot de passe sera stocké en toute sécurité sur cet appareil.",
|
||||
"login_repeaterDescription": "Entrez le mot de passe du répétiteur pour accéder aux paramètres et à l'état.",
|
||||
"login_repeaterDescription": "Entrez le mot de passe du répéteur pour accéder aux paramètres et à l'état.",
|
||||
"login_roomDescription": "Entrez le mot de passe de la pièce pour accéder aux paramètres et à l'état.",
|
||||
"login_routing": "Redirection",
|
||||
"login_routingMode": "Mode de routage",
|
||||
@@ -859,17 +876,17 @@
|
||||
},
|
||||
"path_tooLong": "Le chemin est trop long. Maximum 64 sauts autorisés.",
|
||||
"path_setPath": "Définir le chemin",
|
||||
"repeater_management": "Gestion des répétiteurs",
|
||||
"repeater_management": "Gestion des répéteurs",
|
||||
"repeater_managementTools": "Outils de Gestion",
|
||||
"repeater_status": "État",
|
||||
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répétiteur",
|
||||
"repeater_statusSubtitle": "Afficher l'état, les statistiques et les voisins du répéteur",
|
||||
"repeater_telemetry": "Télémetrie",
|
||||
"repeater_telemetrySubtitle": "Afficher la télémétrie des capteurs et les statistiques du système",
|
||||
"repeater_cli": "CLI",
|
||||
"repeater_cliSubtitle": "Envoyer des commandes au répétiteur",
|
||||
"repeater_cliSubtitle": "Envoyer des commandes au répéteur",
|
||||
"repeater_settings": "Paramètres",
|
||||
"repeater_settingsSubtitle": "Configurer les paramètres du répétiteur",
|
||||
"repeater_statusTitle": "État du répétiteur",
|
||||
"repeater_settingsSubtitle": "Configurer les paramètres du répéteur",
|
||||
"repeater_statusTitle": "État du répéteur",
|
||||
"repeater_routingMode": "Mode de routage",
|
||||
"repeater_autoUseSavedPath": "Auto (utiliser le chemin sauvegardé)",
|
||||
"repeater_forceFloodMode": "Mode tout le réseau forcé",
|
||||
@@ -899,7 +916,7 @@
|
||||
"repeater_packetStatistics": "Statistiques des paquets",
|
||||
"repeater_sent": "Envoyé",
|
||||
"repeater_received": "Reçu",
|
||||
"repeater_duplicates": "Dupliques",
|
||||
"repeater_duplicates": "Doublons",
|
||||
"repeater_daysHoursMinsSecs": "{days} jours {hours}h {minutes}m {seconds}s",
|
||||
"@repeater_daysHoursMinsSecs": {
|
||||
"placeholders": {
|
||||
@@ -964,10 +981,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_settingsTitle": "Paramètres du répétiteur",
|
||||
"repeater_settingsTitle": "Paramètres du répéteur",
|
||||
"repeater_basicSettings": "Paramètres de base",
|
||||
"repeater_repeaterName": "Nom du répétiteur",
|
||||
"repeater_repeaterNameHelper": "Afficher le nom de ce répétiteur",
|
||||
"repeater_repeaterName": "Nom du répéteur",
|
||||
"repeater_repeaterNameHelper": "Afficher le nom de ce répéteur",
|
||||
"repeater_adminPassword": "Mot de passe Administrateur",
|
||||
"repeater_adminPasswordHelper": "Mot de passe d'accès complet",
|
||||
"repeater_guestPassword": "Mot de passe invité",
|
||||
@@ -987,7 +1004,7 @@
|
||||
"repeater_longitudeHelper": "Degrés décimaux (par exemple, -122,4194)",
|
||||
"repeater_features": "Fonctionnalités",
|
||||
"repeater_packetForwarding": "Transfert de paquets",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répétiteur pour transmettre des paquets",
|
||||
"repeater_packetForwardingSubtitle": "Activer le répéteur pour transmettre des paquets",
|
||||
"repeater_guestAccess": "Accès Invité",
|
||||
"repeater_guestAccessSubtitle": "Autoriser l'accès invité en lecture seule",
|
||||
"repeater_privacyMode": "Mode de confidentialité",
|
||||
@@ -1012,16 +1029,16 @@
|
||||
}
|
||||
},
|
||||
"repeater_encryptedAdvertInterval": "Intervalle d'annonces cryptées",
|
||||
"repeater_dangerZone": "Zone d'alerte",
|
||||
"repeater_dangerZone": "Zone dangereuse",
|
||||
"repeater_rebootRepeater": "Redémarrer Répéteur",
|
||||
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répétiteur",
|
||||
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répétiteur ?",
|
||||
"repeater_rebootRepeaterSubtitle": "Réinitialiser l'appareil répéteur",
|
||||
"repeater_rebootRepeaterConfirm": "Êtes-vous sûr de vouloir redémarrer ce répéteur ?",
|
||||
"repeater_regenerateIdentityKey": "Ré générer la clé d'identité",
|
||||
"repeater_regenerateIdentityKeySubtitle": "Générer une nouvelle paire de clés publique/privée",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répétiteur. Continuer ?",
|
||||
"repeater_regenerateIdentityKeyConfirm": "Cela générera une nouvelle identité pour le répéteur. Continuer ?",
|
||||
"repeater_eraseFileSystem": "Supprimer le système de fichiers",
|
||||
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répétiteur",
|
||||
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répétiteur. Cela ne peut pas être annulé !",
|
||||
"repeater_eraseFileSystemSubtitle": "Formater le système de fichiers du répéteur",
|
||||
"repeater_eraseFileSystemConfirm": "AVERTISSEMENT : Cela effacera toutes les données du répéteur. Cela ne peut pas être annulé !",
|
||||
"repeater_eraseSerialOnly": "Erase n'est disponible que via la console série.",
|
||||
"repeater_commandSent": "Commande envoyée : {command}",
|
||||
"@repeater_commandSent": {
|
||||
@@ -1073,7 +1090,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliTitle": "Répétiteur CLI",
|
||||
"repeater_cliTitle": "Répéteur CLI",
|
||||
"repeater_debugNextCommand": "Déboguer Prochaine Commande",
|
||||
"repeater_commandHelp": "Aide",
|
||||
"repeater_clearHistory": "Effacer l'historique",
|
||||
@@ -1107,13 +1124,13 @@
|
||||
"repeater_cliHelpClearStats": "Réinitialise divers compteurs de statistiques à zéro.",
|
||||
"repeater_cliHelpSetAf": "Définit le facteur de temps d'air.",
|
||||
"repeater_cliHelpSetTx": "Définit la puissance de transmission LoRa en dBm (réinitialisation requise pour appliquer).",
|
||||
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répétiteur pour ce nœud.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Serveur de pièce) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
|
||||
"repeater_cliHelpSetRepeat": "Active ou désactive le rôle du répéteur pour ce nœud.",
|
||||
"repeater_cliHelpSetAllowReadOnly": "(Room server) Si \"activé\", alors un mot de passe vide permettra la connexion, mais ne permettra pas de publier dans la pièce. (lecture seule uniquement)",
|
||||
"repeater_cliHelpSetFloodMax": "Définit le nombre maximal de sauts pour les paquets de balayage entrants (si >= max, le paquet n'est pas acheminé).",
|
||||
"repeater_cliHelpSetIntThresh": "Définit le seuil d'interférence (en dB). La valeur par défaut est de 14. Définir sur 0 désactive la détection des interférences de canal.",
|
||||
"repeater_cliHelpSetAgcResetInterval": "Définit l'intervalle pour réinitialiser le contrôleur de gain automatique. Mettez à 0 pour désactiver.",
|
||||
"repeater_cliHelpSetMultiAcks": "Active ou désactive la fonctionnalité « double ACKs ».",
|
||||
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle du minuteur pour envoyer un paquet d'annonce local (sans relais). Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetAdvertInterval": "Définit l'intervalle entre chaque émission d'une annonce locale (sans relais). Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetFloodAdvertInterval": "Définit l'intervalle du minuteur en heures pour envoyer un paquet d'annonce massive. Définir sur 0 pour désactiver.",
|
||||
"repeater_cliHelpSetGuestPassword": "Définit/met à jour le mot de passe de l'invité. (pour les répéteurs, les connexions d'invités peuvent envoyer la requête \"Get Stats\")",
|
||||
"repeater_cliHelpSetName": "Définit le nom de l'annonce.",
|
||||
@@ -1135,7 +1152,7 @@
|
||||
"repeater_cliHelpLogStart": "Démarre l'enregistrement des paquets dans le système de fichiers.",
|
||||
"repeater_cliHelpLogStop": "Arrêter de journaliser les paquets vers le système de fichiers.",
|
||||
"repeater_cliHelpLogErase": "Supprime les journaux de paquets du système de fichiers.",
|
||||
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répétiteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
||||
"repeater_cliHelpNeighbors": "Affiche une liste d'autres nœuds répéteurs entendus via des annonces sans relais. Chaque ligne est id-préfixe-hexadécimal:timestamp:snr-fois-4",
|
||||
"repeater_cliHelpNeighborRemove": "Supprime la première entrée correspondante (par préfixe de clé publique (hexadécimal)) de la liste des voisins.",
|
||||
"repeater_cliHelpRegion": "(série uniquement) Liste toutes les régions définies et les autorisations actuelles d'annonces sur tout le réseau (flood).",
|
||||
"repeater_cliHelpRegionLoad": "REMARQUE : il s'agit d'une invocation multi-commande spéciale. Chaque commande subséquente est un nom de région (indenté avec des espaces pour indiquer la hiérarchie parent, avec un minimum d'un espace). Terminé par l'envoi d'une ligne vide/commande.",
|
||||
@@ -1159,8 +1176,8 @@
|
||||
"repeater_settingsCategory": "Paramètres",
|
||||
"repeater_bridge": "Pont",
|
||||
"repeater_logging": "Journalisation",
|
||||
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répétiteur)",
|
||||
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répétiteur)",
|
||||
"repeater_neighborsRepeaterOnly": "Voisins (Uniquement répéteur)",
|
||||
"repeater_regionManagementRepeaterOnly": "Gestion des régions (uniquement pour le répéteur)",
|
||||
"repeater_regionNote": "Les commandes de région ont été introduites pour gérer les définitions et les autorisations des régions.",
|
||||
"repeater_gpsManagement": "Gestion GPS",
|
||||
"repeater_gpsNote": "La commande GPS a été introduite pour gérer les sujets liés à la localisation.",
|
||||
@@ -1229,7 +1246,7 @@
|
||||
"channelPath_title": "Chemin de paquet",
|
||||
"channelPath_viewMap": "Afficher la carte",
|
||||
"channelPath_otherObservedPaths": "Autres chemins observés",
|
||||
"channelPath_repeaterHops": "Sauts du répétiteur",
|
||||
"channelPath_repeaterHops": "Sauts du répéteur",
|
||||
"channelPath_noHopDetails": "Les détails de l'envoi ne sont pas fournis pour ce paquet.",
|
||||
"channelPath_messageDetails": "Détails du message",
|
||||
"channelPath_senderLabel": "Expéditeur",
|
||||
@@ -1294,7 +1311,7 @@
|
||||
}
|
||||
},
|
||||
"channelPath_mapTitle": "Carte du chemin",
|
||||
"channelPath_noRepeaterLocations": "Aucune position de répétiteur disponible pour ce chemin.",
|
||||
"channelPath_noRepeaterLocations": "Aucune position de répéteur disponible pour ce chemin.",
|
||||
"channelPath_primaryPath": "Chemin {index} (Principal)",
|
||||
"@channelPath_primaryPath": {
|
||||
"placeholders": {
|
||||
@@ -1327,16 +1344,16 @@
|
||||
"channelPath_unknownRepeater": "Répéteur Inconnu",
|
||||
"listFilter_tooltip": "Filtrer et trier",
|
||||
"listFilter_sortBy": "Trier par",
|
||||
"listFilter_latestMessages": "Dernières messages",
|
||||
"listFilter_latestMessages": "Derniers messages",
|
||||
"listFilter_heardRecently": "Écoute récemment",
|
||||
"listFilter_az": "A à Z",
|
||||
"listFilter_filters": "Filtres",
|
||||
"listFilter_all": "Tout",
|
||||
"listFilter_users": "Utilisateurs",
|
||||
"listFilter_repeaters": "Répéteurs",
|
||||
"listFilter_roomServers": "Serveurs de pièce",
|
||||
"listFilter_roomServers": "Room servers",
|
||||
"listFilter_unreadOnly": "Messages non lus seulement",
|
||||
"listFilter_newGroup": "Nouvelle groupe",
|
||||
"listFilter_newGroup": "Nouveau groupe",
|
||||
"@neighbors_errorLoading": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Voisins",
|
||||
"repeater_neighboursSubtitle": "Afficher les voisins de saut nuls.",
|
||||
"repeater_neighbors": "Voisins",
|
||||
"repeater_neighborsSubtitle": "Afficher les voisins de saut nuls.",
|
||||
"neighbors_receivedData": "Données des voisins reçues",
|
||||
"neighbors_requestTimedOut": "Les voisins demandent un délai.",
|
||||
"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.",
|
||||
"channels_createPrivateChannelDesc": "Sécurisé avec une clé secrète.",
|
||||
"channels_joinPrivateChannel": "Rejoindre un Canal Privé",
|
||||
@@ -1362,7 +1379,7 @@
|
||||
"channels_scanQrCode": "Scanner un code QR",
|
||||
"channels_scanQrCodeComingSoon": "Bientôt disponible",
|
||||
"channels_enterHashtag": "Entrez le hashtag",
|
||||
"channels_hashtagHint": "ex. #équipe",
|
||||
"channels_hashtagHint": "ex. #equipe",
|
||||
"@neighbors_unknownContact": {
|
||||
"placeholders": {
|
||||
"pubkey": {
|
||||
@@ -1379,12 +1396,12 @@
|
||||
},
|
||||
"neighbors_unknownContact": "Clé publique inconnue {pubkey}",
|
||||
"neighbors_heardAgo": "Écouté : {time} auparavant",
|
||||
"settings_locationGPSEnable": "Habilita GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Habilita la actualización automática de la ubicación mediante GPS.",
|
||||
"settings_locationIntervalSec": "Intervalo pour GPS (Segundos)",
|
||||
"settings_locationIntervalInvalid": "El intervalo debe ser de al menos 60 segundos y menor que 86400 segundos.",
|
||||
"contacts_manageRoom": "Gestionar Servidor de Habitación",
|
||||
"room_management": "Administración del Servidor de Habitación",
|
||||
"settings_locationGPSEnable": "Activer le GPS",
|
||||
"settings_locationGPSEnableSubtitle": "Activer la mise à jour automatique de la position via GPS",
|
||||
"settings_locationIntervalSec": "Intervalle de mise-à-jour du GPS (Secondes)",
|
||||
"settings_locationIntervalInvalid": "L'intervalle doit être compris entre 60 et 86400 secondes.",
|
||||
"contacts_manageRoom": "Gérer le Room Server",
|
||||
"room_management": "Administrattion Room Server",
|
||||
"@community_joinConfirmation": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -1473,16 +1490,374 @@
|
||||
"community_deleteChannelsWarning": "Cela supprimera également {count} canal/canaux et leurs messages.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Communauté \"{name}\" quittée",
|
||||
"community_addHashtagChannel": "Ajouter un Hashtag Communauté",
|
||||
"community_addHashtagChannelDesc": "Ajouter un canal hachage pour cette communauté",
|
||||
"community_addHashtagChannelDesc": "Ajouter un canal hashtag pour cette communauté",
|
||||
"community_selectCommunity": "Sélectionner Communauté",
|
||||
"community_regularHashtag": "Hashtag régulier",
|
||||
"community_regularHashtagDesc": "Hashtag public (tout le monde peut rejoindre)",
|
||||
"community_communityHashtag": "Hashtag de la communauté",
|
||||
"community_communityHashtagDesc": "Exclusif aux membres de la communauté",
|
||||
"community_forCommunity": "Pour {name}"
|
||||
"community_forCommunity": "Pour {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"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_regenerate": "Régénérer",
|
||||
"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_updateSecret": "Mettre à jour le secret",
|
||||
"community_secretUpdated": "Modification secrète mise à jour pour \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Vous",
|
||||
"pathTrace_refreshTooltip": "Actualiser Path Trace",
|
||||
"pathTrace_failed": "Traçage du chemin échoué.",
|
||||
"pathTrace_notAvailable": "Tracé de chemin non disponible.",
|
||||
"contacts_pathTrace": "Traçage de chemin",
|
||||
"contacts_repeaterPathTrace": "Tracer le chemin vers le répéteur",
|
||||
"contacts_repeaterPing": "Pinguer le répéteur",
|
||||
"contacts_roomPathTrace": "Traçage du chemin vers le serveur de la salle",
|
||||
"contacts_chatTraceRoute": "Tracer le chemin",
|
||||
"contacts_pathTraceTo": "Tracer l'itinéraire vers {name}",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_roomPing": "Pinguer le serveur de la salle",
|
||||
"contacts_invalidAdvertFormat": "Données de contact non valides",
|
||||
"appSettings_languageUk": "Ukrainien",
|
||||
"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_contactImported": "Le contact a été importé.",
|
||||
"contacts_floodAdvert": "Annonce à tout le réseau",
|
||||
"contacts_contactImportFailed": "Échec de l'importation du contact.",
|
||||
"contacts_zeroHopAdvert": "Annonce Zero saut",
|
||||
"contacts_copyAdvertToClipboard": "Copier l'annonce dans le presse-papiers",
|
||||
"contacts_addContactFromClipboard": "Ajouter un contact depuis le presse-papiers",
|
||||
"contacts_ShareContact": "Copier le contact dans le presse-papiers",
|
||||
"contacts_ShareContactZeroHop": "Partager un contact par annonce",
|
||||
"contacts_contactAdvertCopied": "Annonce copiée dans le presse-papiers.",
|
||||
"contacts_contactAdvertCopyFailed": "La copie de l'annonce vers le presse-papiers a échoué.",
|
||||
"contacts_zeroHopContactAdvertSent": "Envoyer un contact par annonce.",
|
||||
"contacts_zeroHopContactAdvertFailed": "Échec de l'envoi du contact.",
|
||||
"notification_activityTitle": "Activité MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{message} other{messages}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{message de canal} other{messages de canal}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nouveau nœud} other{nouveaux nœuds}}",
|
||||
"notification_newTypeDiscovered": "Nouveau {contactType} découvert",
|
||||
"notification_receivedNewMessage": "Nouveau message reçu",
|
||||
"settings_gpxExportRepeaters": "Exporter les répéteurs / serveur de salle au format GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporte les répéteurs / roomserver avec une localisation vers un fichier GPX.",
|
||||
"settings_gpxExportNoContacts": "Aucun contact à exporter.",
|
||||
"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_gpxExportRepeatersRoom": "Emplacements des serveurs de répéteur et de salle",
|
||||
"settings_gpxExportContacts": "Exporter les compagnons 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_gpxExportContactsSubtitle": "Exporte les compagnons avec un emplacement vers un fichier GPX.",
|
||||
"settings_gpxExportChat": "Emplacements des compagnons",
|
||||
"settings_gpxExportSuccess": "Fichier GPX exporté avec succès.",
|
||||
"settings_gpxExportAllContacts": "Tous les emplacements des contacts",
|
||||
"settings_gpxExportShareText": "Données de carte exportées à partir de meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exporter les données de carte GPX",
|
||||
"pathTrace_someHopsNoLocation": "Un ou plusieurs des sauts manquent d'une localisation !",
|
||||
"map_tapToAdd": "Appuyez sur les nœuds pour les ajouter au chemin.",
|
||||
"pathTrace_clearTooltip": "Effacer le chemin",
|
||||
"map_pathTraceCancelled": "Traçage de chemin annulé",
|
||||
"map_removeLast": "Supprimer le dernier",
|
||||
"map_runTrace": "Exécuter la traçage de chemin",
|
||||
"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_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_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_aboutOpenMeteoAttribution": "Données d'élévation LOS : Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Unités",
|
||||
"appSettings_unitsMetric": "Métrique (m/km)",
|
||||
"appSettings_unitsImperial": "Impérial (ft / mi)",
|
||||
"map_lineOfSight": "Ligne de vue",
|
||||
"map_losScreenTitle": "Ligne de vue",
|
||||
"losSelectStartEnd": "Sélectionnez les nœuds de début et de fin pour LOS.",
|
||||
"losRunFailed": "Échec de la vérification de la ligne de vue : {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Effacer tous les points",
|
||||
"losRunToViewElevationProfile": "Exécutez LOS pour afficher le profil d'altitude",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Appuyez sur les nœuds ou appuyez longuement sur la carte pour des points personnalisés",
|
||||
"losShowDisplayNodes": "Afficher les nœuds d'affichage",
|
||||
"losCustomPoints": "Points personnalisés",
|
||||
"losCustomPointLabel": "Personnalisé {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Point A",
|
||||
"losPointB": "Point B",
|
||||
"losAntennaA": "Antenne A : {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antenne B : {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Exécuter la LOS",
|
||||
"losNoElevationData": "Aucune donnée d'altitude",
|
||||
"losProfileClear": "{distance} {distanceUnit}, LOS clair, clairance minimale {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, bloqué par {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS : vérification...",
|
||||
"losStatusNoData": "LOS : aucune donnée",
|
||||
"losStatusSummary": "LOS : {clear}/{total} clair, {blocked} bloqué, {unknown} inconnu",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Données d'altitude indisponibles pour un ou plusieurs échantillons.",
|
||||
"losErrorInvalidInput": "Données de points/d'altitude non valides pour le calcul de la LOS.",
|
||||
"losRenameCustomPoint": "Renommer le point personnalisé",
|
||||
"losPointName": "Nom du point",
|
||||
"losShowPanelTooltip": "Afficher le panneau LOS",
|
||||
"losHidePanelTooltip": "Masquer le panneau LOS",
|
||||
"losElevationAttribution": "Données d’altitude : Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horizon radio",
|
||||
"losLegendLosBeam": "Ligne de visée",
|
||||
"losLegendTerrain": "Terrain",
|
||||
"losFrequencyLabel": "Fréquence",
|
||||
"losFrequencyInfoTooltip": "Voir les détails du calcul",
|
||||
"losFrequencyDialogTitle": "Calcul de l’horizon radio",
|
||||
"losFrequencyDialogDescription": "À partir de k={baselineK} à {baselineFreq} MHz, le calcul ajuste le facteur k pour la bande actuelle de {frequencyMHz} MHz, ce qui définit la limite incurvée de l'horizon radio.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_addToFavorites": "Ajouter à mes favoris",
|
||||
"listFilter_removeFromFavorites": "Supprimer des favoris",
|
||||
"listFilter_favorites": "Préférences",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Non lu",
|
||||
"contacts_searchFavorites": "Rechercher {number}{str} Favoris...",
|
||||
"contacts_searchUsers": "Rechercher {number}{str} utilisateurs...",
|
||||
"contacts_searchRoomServers": "Rechercher {number}{str} serveurs de salle...",
|
||||
"contacts_searchRepeaters": "Rechercher {number}{str} Répéteurs...",
|
||||
"contacts_searchContactsNoNumber": "Rechercher des contacts...",
|
||||
"settings_contactSettings": "Paramètres de contact",
|
||||
"settings_contactSettingsSubtitle": "Paramètres pour l'ajout de contacts",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Ajouter automatiquement les répéteurs",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Autoriser le compagnon à ajouter automatiquement les répéteurs découverts",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Ajouter automatiquement les serveurs de salle",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Autoriser le compagnon à ajouter automatiquement les serveurs de salles découverts",
|
||||
"contactsSettings_otherTitle": "Autres paramètres liés aux contacts",
|
||||
"contactsSettings_title": "Paramètres des contacts",
|
||||
"contactsSettings_autoAddUsersTitle": "Ajouter automatiquement les utilisateurs",
|
||||
"contactsSettings_autoAddTitle": "Découverte automatique",
|
||||
"contactsSettings_autoAddSensorsTitle": "Ajouter automatiquement les capteurs",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Autoriser le compagnon à ajouter automatiquement les utilisateurs découverts",
|
||||
"discoveredContacts_noMatching": "Aucun contact correspondant",
|
||||
"discoveredContacts_contactAdded": "Contact ajouté",
|
||||
"discoveredContacts_addContact": "Ajouter un contact",
|
||||
"discoveredContacts_copyContact": "Copier le contact dans le presse-papiers",
|
||||
"discoveredContacts_deleteContact": "Supprimer le contact",
|
||||
"contactsSettings_overwriteOldestTitle": "Écraser le plus ancien",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Autoriser le compagnon à ajouter automatiquement les capteurs découverts.",
|
||||
"discoveredContacts_Title": "Contacts découverts",
|
||||
"discoveredContacts_searchHint": "Rechercher des contacts découverts",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Lorsque la liste de contacts est pleine, le contact le plus ancien non favori sera remplacé.",
|
||||
"common_deleteAll": "Supprimer tout",
|
||||
"discoveredContacts_deleteContactAll": "Supprimer tous les contacts découverts",
|
||||
"discoveredContacts_deleteContactAllContent": "Êtes-vous sûr de vouloir supprimer tous les contacts découverts ?",
|
||||
"map_showGuessedLocations": "Afficher les emplacements des nœuds estimés",
|
||||
"map_guessedLocation": "Lieu deviné",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"usbScreenTitle": "Connectez via USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenSubtitle": "Sélectionnez un périphérique série détecté et connectez-vous directement à votre nœud MeshCore.",
|
||||
"usbScreenStatus": "Sélectionnez un périphérique USB",
|
||||
"usbScreenNote": "La communication série USB est active sur les appareils Android et les plateformes de bureau compatibles.",
|
||||
"usbScreenEmptyState": "Aucun périphérique USB n'a été trouvé. Veuillez en brancher un et rafraîchir la page.",
|
||||
"usbErrorPermissionDenied": "L'accès via USB a été refusé.",
|
||||
"usbErrorDeviceMissing": "Le périphérique USB sélectionné n'est plus disponible.",
|
||||
"usbErrorInvalidPort": "Sélectionnez un périphérique USB valide.",
|
||||
"usbErrorBusy": "Une autre demande de connexion USB est déjà en cours.",
|
||||
"usbErrorNotConnected": "Aucun appareil USB n'est connecté.",
|
||||
"usbErrorOpenFailed": "Impossible d'ouvrir l'appareil USB sélectionné.",
|
||||
"usbErrorConnectFailed": "Impossible de se connecter à l'appareil USB sélectionné.",
|
||||
"usbErrorUnsupported": "La communication série USB n'est pas prise en charge sur cette plateforme.",
|
||||
"usbErrorAlreadyActive": "Une connexion USB est déjà établie.",
|
||||
"usbErrorNoDeviceSelected": "Aucun appareil USB n'a été sélectionné.",
|
||||
"usbErrorPortClosed": "La connexion USB n'est pas établie.",
|
||||
"usbFallbackDeviceName": "Dispositif de communication série sur le Web",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_notConnected": "Sélectionnez un périphérique USB",
|
||||
"usbConnectionFailed": "Échec de la connexion USB : {error}",
|
||||
"usbStatus_connecting": "Connexion au périphérique USB...",
|
||||
"usbStatus_searching": "Recherche de périphériques USB...",
|
||||
"usbErrorConnectTimedOut": "La connexion a expiré. Assurez-vous que l'appareil dispose du firmware USB Companion."
|
||||
}
|
||||
|
||||
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Impossibile eliminare il canale \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "it",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contatti",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Numero contatti",
|
||||
"settings_infoChannelCount": "Numero Canale",
|
||||
"settings_presets": "Preset",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequenza (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frequenza non valida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potenza (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potere TX non valido (0-22 dBm)",
|
||||
"settings_longRange": "Lungo Raggio",
|
||||
"settings_fastSpeed": "Velocità Rapida",
|
||||
"settings_error": "Errore: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canale pubblico",
|
||||
"channels_privateChannel": "Canale privato",
|
||||
"channels_editChannel": "Modifica canale",
|
||||
"channels_muteChannel": "Silenzia canale",
|
||||
"channels_unmuteChannel": "Attiva notifiche canale",
|
||||
"channels_deleteChannel": "Elimina canale",
|
||||
"channels_deleteChannelConfirm": "Eliminare \"{name}\"? Non può essere annullato.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Aprire il link?",
|
||||
"chat_openLinkConfirmation": "Vuoi aprire questo link nel tuo browser?",
|
||||
"chat_open": "Apri",
|
||||
"chat_couldNotOpenLink": "Impossibile aprire il link: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Formato di link non valido",
|
||||
"map_title": "Mappa Nodi",
|
||||
"map_noNodesWithLocation": "Nessun nodo con dati di posizione",
|
||||
"map_nodesNeedGps": "I nodi devono condividere le loro coordinate GPS\nper apparire sulla mappa",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vicini",
|
||||
"repeater_neighboursSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||
"repeater_neighbors": "Vicini",
|
||||
"repeater_neighborsSubtitle": "Visualizza vicini di salto pari a zero.",
|
||||
"neighbors_receivedData": "Ricevute dati vicini",
|
||||
"neighbors_requestTimedOut": "I vicini richiedono un timeout.",
|
||||
"neighbors_errorLoading": "Errore nel caricamento dei vicini: {error}",
|
||||
"neighbors_repeatersNeighbours": "Ripetitori Vicini",
|
||||
"neighbors_repeatersNeighbors": "Ripetitori Vicini",
|
||||
"neighbors_noData": "Nessun dato sugli vicini disponibile.",
|
||||
"channels_createPrivateChannel": "Crea un Canale Privato",
|
||||
"channels_createPrivateChannelDesc": "Protetta con una chiave segreta.",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Questo eliminerà anche {count} canale/i e i loro messaggi.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Hai lasciato la comunità \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Hashtag pubblico (chiunque può unirsi)",
|
||||
"community_communityHashtag": "Hashtag della Comunità",
|
||||
"community_communityHashtagDesc": "Visibile solo ai membri della comunità",
|
||||
"community_forCommunity": "Per {name}"
|
||||
"community_forCommunity": "Per {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecretConfirm": "Regenera la chiave segreta per \"{name}\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.",
|
||||
"community_regenerateSecret": "Ri genera la chiave segreta",
|
||||
"community_regenerate": "Rigenera",
|
||||
"community_secretRegenerated": "Codice segreto rigenerato per \"{name}\"",
|
||||
"community_updateSecret": "Aggiorna Segreto",
|
||||
"community_secretUpdated": "Segreto aggiornato per \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scansiona il nuovo codice QR per aggiornare il segreto di \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_failed": "Tracciamento del percorso fallito.",
|
||||
"pathTrace_you": "Tu",
|
||||
"pathTrace_notAvailable": "Tracciamento del percorso non disponibile.",
|
||||
"pathTrace_refreshTooltip": "Aggiorna Path Trace.",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_repeaterPathTrace": "Traccia percorso al ripetitore",
|
||||
"contacts_roomPathTrace": "Traccia del percorso al server della stanza",
|
||||
"contacts_pathTrace": "Traccia Percorso",
|
||||
"contacts_repeaterPing": "Ripetitore ping",
|
||||
"contacts_pathTraceTo": "Traccia percorso verso {name}",
|
||||
"contacts_roomPing": "Ping al server della stanza",
|
||||
"contacts_chatTraceRoute": "Traccia percorso path",
|
||||
"appSettings_languageRu": "Russo",
|
||||
"contacts_invalidAdvertFormat": "Dati di contatto non validi",
|
||||
"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_floodAdvert": "Annuncio alluvionale",
|
||||
"contacts_copyAdvertToClipboard": "Copia Annuncio negli Appunti",
|
||||
"contacts_addContactFromClipboard": "Aggiungere contatto dalla clipboard",
|
||||
"contacts_clipboardEmpty": "La clipboard è vuota.",
|
||||
"contacts_ShareContact": "Copia contatto negli Appunti",
|
||||
"contacts_contactImported": "Il contatto è stato importato.",
|
||||
"contacts_contactImportFailed": "Contatto non importato con successo.",
|
||||
"contacts_zeroHopContactAdvertSent": "Inviato contatto tramite annuncio.",
|
||||
"contacts_contactAdvertCopyFailed": "Copia dell'annuncio nella Clipboard non riuscita.",
|
||||
"contacts_ShareContactZeroHop": "Condividi contatto tramite annuncio",
|
||||
"contacts_zeroHopContactAdvertFailed": "Invio del contatto non riuscito.",
|
||||
"contacts_contactAdvertCopied": "Annuncio copiato negli Appunti.",
|
||||
"notification_activityTitle": "Attività MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{messaggio} other{messaggi}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{messaggio del canale} other{messaggi del canale}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nuovo nodo} other{nuovi nodi}}",
|
||||
"notification_newTypeDiscovered": "Nuovo {contactType} scoperto",
|
||||
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
|
||||
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
|
||||
"settings_gpxExportContacts": "Esporta compagni in GPX",
|
||||
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
|
||||
"settings_gpxExportNoContacts": "Nessun contatto da esportare.",
|
||||
"settings_gpxExportNotAvailable": "Non supportato sul tuo dispositivo/Sistema Operativo",
|
||||
"settings_gpxExportError": "Si è verificato un errore durante l'esportazione.",
|
||||
"settings_gpxExportRepeatersSubtitle": "Esporta ripetitori / roomserver con una posizione in un file GPX.",
|
||||
"settings_gpxExportContactsSubtitle": "Esporta i compagni con una posizione in un file GPX.",
|
||||
"settings_gpxExportAll": "Esporta tutti i contatti in GPX",
|
||||
"settings_gpxExportAllSubtitle": "Esporta tutti i contatti con una posizione in un file GPX.",
|
||||
"settings_gpxExportChat": "Posizioni dei compagni",
|
||||
"settings_gpxExportRepeatersRoom": "Posizioni del server ripetitore e della stanza",
|
||||
"settings_gpxExportAllContacts": "Tutte le posizioni dei contatti",
|
||||
"settings_gpxExportShareText": "Dati mappa esportati da meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open esportazione dati mappa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Uno o più dei luppoli mancano di una posizione!",
|
||||
"map_removeLast": "Rimuovi ultimo",
|
||||
"map_pathTraceCancelled": "Tracciamento del percorso annullato.",
|
||||
"pathTrace_clearTooltip": "Pulisci percorso",
|
||||
"map_runTrace": "Esegui Path Trace",
|
||||
"map_tapToAdd": "Tocca i nodi per aggiungerli al percorso.",
|
||||
"scanner_bluetoothOff": "Il Bluetooth è disattivato.",
|
||||
"scanner_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",
|
||||
"snrIndicator_nearByRepeaters": "Ripetitori vicini",
|
||||
"snrIndicator_lastSeen": "Ultimo accesso",
|
||||
"chat_ShowAllPaths": "Mostra tutti i percorsi",
|
||||
"settings_clientRepeat": "Ripetizione \"fuori dalla rete\"",
|
||||
"settings_clientRepeatFreqWarning": "Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.",
|
||||
"settings_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."
|
||||
}
|
||||
|
||||
+1093
-42
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Изтрий';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Изтрий всичко';
|
||||
|
||||
@override
|
||||
String get common_close => 'Затвори';
|
||||
|
||||
@@ -108,6 +111,90 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Свържете се чрез USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Изберете открития сериен уред и свържете директно към вашия MeshCore възел.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Изберете USB устройство';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB серийната връзка е активна на поддържаните Android устройства и настолни платформи.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Няма открити USB устройства. Включете едно и опитайте отново.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Не беше разрешено достъпът през USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Избраното USB устройство вече не е налично.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Изберете валитно USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Друг мол за свързване през USB вече е в процес на изпълнение.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Няма свързано USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Не успях да отворя избраното USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Не успях да се свържа с избраното USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB серийната комуникация не се поддържа на тази платформа.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'USB връзката вече е активирана.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Няма избран USB устройство.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB връзката не е активна.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Връзката прекъсна. Уверете се, че устройството има софтуер за USB връзка.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Устройство за четене на уеб серийни данни';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Изберете USB устройство';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Свързване към USB устройство...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Търсене на USB устройства...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Неуспешно свързване през USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Сканиране за устройства...';
|
||||
|
||||
@@ -143,6 +230,23 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Сканирай';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth е изключен.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Моля, активирайте Bluetooth, за да сканирате за устройства.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Изисква се браузър Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Това уеб приложение изисква Google Chrome или браузър, базиран на Chromium, за поддръжка на Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Активирайте Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Бързо превключване';
|
||||
|
||||
@@ -224,6 +328,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Дължина';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Настройки за контакти';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Настройки за добавяне на контакти.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Режим на поверителност';
|
||||
|
||||
@@ -316,6 +427,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Отворен софтуер за Flutter клиент за MeshCore LoRa мрежови устройства.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Данни за надморска височина на LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Име';
|
||||
|
||||
@@ -340,15 +455,6 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Предварителни настройки';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Честота (MHz)';
|
||||
|
||||
@@ -377,10 +483,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Невалидна мощност на TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Дълъг обхват';
|
||||
String get settings_clientRepeat => 'Без електричество – повторение';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Бърза скорост';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Позволете на това устройство да предава пакети към мрежата за други устройства.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'За повторение извън мрежата са необходими честоти от 433, 869 или 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -450,6 +561,20 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Руски';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Украински';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Разрешаване на проследяване на съобщения';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Показване на подробни метаданни за маршрутизация и синхронизация за съобщения';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Уведомления';
|
||||
|
||||
@@ -610,6 +735,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'единици';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Метрика (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Имперска (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Няма избрана област';
|
||||
|
||||
@@ -648,7 +782,35 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
'Контактите ще се появят, когато устройствата рекламират.';
|
||||
|
||||
@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
|
||||
String get contacts_noUnreadContacts => 'Няма непрочетени контакти';
|
||||
@@ -773,6 +935,12 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Редактирай канал';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Заглуши канала';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Включи известията на канала';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Изтрий канала';
|
||||
|
||||
@@ -781,6 +949,11 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Изтрий \"$name\"? Това не може да бъде отменено.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Неуспешно изтриване на канала \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Каналът \"$name\" е изтрит';
|
||||
@@ -1068,6 +1241,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Управление на пътища';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Покажи всички пътища';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Режим на маршрутизиране';
|
||||
|
||||
@@ -1207,9 +1383,33 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Непрочетени: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Отваряне на връзката?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Искате ли да отворите тази връзка в браузъра си?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Отвори';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Не можа да се отвори връзката: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Невалиден формат на връзката';
|
||||
|
||||
@override
|
||||
String get map_title => 'Карта на възлите';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Линия на видимост';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Линия на видимост';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Няма възли с данни за местоположение.';
|
||||
|
||||
@@ -1327,6 +1527,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Покажи споделени маркери';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Покажете местоположенията на предположените възли.';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Предполагано местоположение';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Последна видяна дата';
|
||||
|
||||
@@ -1339,6 +1546,19 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Управление на Повтарящ се Елемент';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Натиснете върху възлите, за да ги добавите към пътя.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Изпълни Път на Следване';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Премахни Последно';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Отменен е следването на пътя.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Кеш на офлайн карти';
|
||||
|
||||
@@ -1634,10 +1854,10 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Изпрати команди към ретранслатора';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Съседи';
|
||||
String get repeater_neighbors => 'Съседи';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Преглед на съседни възли с нулев скок.';
|
||||
|
||||
@override
|
||||
@@ -2337,7 +2557,7 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Повторители Съседи';
|
||||
String get neighbors_repeatersNeighbors => 'Повторители Съседи';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Няма налични данни за съседи.';
|
||||
@@ -2567,32 +2787,32 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Регенерейрай секрет';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Регенерация на секретния ключ за \"$name\"? Всички членове ще трябва да сканират новия QR код, за да продължат комуникацията.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Регенерация';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Секретно презареждане за \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Актуализирай тайна';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Секретно обновено за \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Сканьорвайте новия QR код, за да актуализирате секрета за \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2644,6 +2864,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Всички';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Любими';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Добави към любими';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Премахване от списъка с любими';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Потребители';
|
||||
|
||||
@@ -2658,4 +2887,415 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Нова група';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Вие';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Пътят за проследяване не успя.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Пътека за проследяване не е достъпна.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Обнови Path Trace.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Един или повече от хмелите липсва местоположение!';
|
||||
|
||||
@override
|
||||
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
|
||||
String get contacts_pathTrace => 'Пътен проследяване';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Пинг';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Трасировка до повторител';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Пингване на повторителя';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Трасиране на път до съ';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping на сървъра на стаята';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Трасиране на път';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Проследи маршрут към $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Клипборда е празна.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Невалидни данни за контакт';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Контактът е импортиран.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Контактът не е успешно импортиран.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Реклама без скок';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Потопна реклама';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Копирай обявата в клипборда';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard => 'Добави контакт от клипборда';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Копирай контакт в клипборда';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Сподели контакт чрез обява';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Изпратен контакт по обява.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Неуспешно изпращане на контакт.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied =>
|
||||
'Рекламата е копирана в клипборда.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Копирането на обявата в клипборда не успя.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Активност на MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'съобщения',
|
||||
one: 'съобщение',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'съобщения в канали',
|
||||
one: 'съобщение в канал',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'нови възли',
|
||||
one: 'нов възел',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Открит нов $contactType';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Получено ново съобщение';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Експортиране на повтарящи се устройства / сървър на стаята до GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Изпраща повторители / roomserver с местоположение в GPX файл.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Експортирай спътници към GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Експортира спътници с местоположение в GPX файл.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Експортирай всички контакти в GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Експортира всички контакти с местоположение в файл GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Успешно изlexport на файл GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Няма контакти за изlexport.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Не е поддържан на вашето устройство/ОС';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Възникна грешка при изнасяне.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Местоположения на повторител и сървър на стаята';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Местоположения на спътници';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts =>
|
||||
'Местоположения на всички контакти';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Картинни данни изнесени от meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'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 =>
|
||||
'Сигурни ли сте, че искате да изтриете всички открити контакти?';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Delete';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Delete All';
|
||||
|
||||
@override
|
||||
String get common_close => 'Close';
|
||||
|
||||
@@ -108,6 +111,88 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connect over USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Choose a detected serial device and connect directly to your MeshCore node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Select a USB device';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB serial is active on supported Android devices and desktop platforms.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No USB devices found. Plug one in and refresh.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'USB permission was denied.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'The selected USB device is no longer available.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Select a valid USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Another USB connection request is already in progress.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'No USB device is connected.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed => 'Failed to open the selected USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Failed to connect to the selected USB device.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB serial is not supported on this platform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'A USB connection is already active.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'No USB device was selected.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'The USB connection is not open.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Connection timed out. Make sure the device has USB Companion firmware.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web Serial Device';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Select a USB device';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Connecting to USB device...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Searching for USB devices...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'USB connection failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scanning for devices...';
|
||||
|
||||
@@ -142,6 +227,23 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scan';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth is off';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Please turn on Bluetooth to scan for devices';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome Browser Required';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'This web application requires Google Chrome or a Chromium-based browser for Bluetooth support.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Enable Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Quick switch';
|
||||
|
||||
@@ -222,6 +324,13 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitude';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Contact Settings';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Settings for how contacts are added.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privacy Mode';
|
||||
|
||||
@@ -308,6 +417,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'An open-source Flutter client for MeshCore LoRa mesh networking devices.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS elevation data: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Name';
|
||||
|
||||
@@ -332,15 +445,6 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequency (MHz)';
|
||||
|
||||
@@ -369,10 +473,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Invalid TX power (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Long Range';
|
||||
String get settings_clientRepeat => 'Off-Grid Repeat';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Fast Speed';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Allow this device to repeat mesh packets for others';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Off-grid repeat requires 433, 869, or 918 MHz frequency';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -442,6 +551,19 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Русский';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Show detailed routing and timing metadata for messages';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notifications';
|
||||
|
||||
@@ -602,6 +724,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Units';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metric (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'No area selected';
|
||||
|
||||
@@ -638,7 +769,35 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
'Contacts will appear when devices advertise';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Search contacts...';
|
||||
String get contacts_unread => 'Unread';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Search Contacts...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Search $number$str Contacts...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Search $number$str Favorites...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Search $number$str Users...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Search $number$str Repeaters...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Search $number$str Room servers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'No unread contacts';
|
||||
@@ -701,27 +860,27 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get contacts_noMembers => 'No members';
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenNow => 'Last seen now';
|
||||
String get contacts_lastSeenNow => 'recently';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Last seen $minutes mins ago';
|
||||
return '~ $minutes min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Last seen 1 hour ago';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 hour';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Last seen $hours hours ago';
|
||||
return '~ $hours hours';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Last seen 1 day ago';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 day';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Last seen $days days ago';
|
||||
return '~ $days days';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -762,6 +921,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Edit channel';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Mute channel';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Unmute channel';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Delete channel';
|
||||
|
||||
@@ -770,6 +935,11 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Delete \"$name\"? This cannot be undone.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Failed to delete channel \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Channel \"$name\" deleted';
|
||||
@@ -1053,6 +1223,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Path Management';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Show all paths';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routing mode';
|
||||
|
||||
@@ -1186,9 +1359,33 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Unread: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Open Link?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Do you want to open this link in your browser?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Open';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Could not open link: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Invalid link format';
|
||||
|
||||
@override
|
||||
String get map_title => 'Node Map';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'No nodes with location data';
|
||||
|
||||
@@ -1306,6 +1503,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Show shared markers';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => 'Show guessed node locations';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Guessed location';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Last Seen Time';
|
||||
|
||||
@@ -1318,6 +1521,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Manage Repeater';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tap on nodes to add them to the path.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Run Path Trace';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remove Last';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Path trace cancelled.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Map Cache';
|
||||
|
||||
@@ -1608,10 +1823,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Send commands to the repeater';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Neighbors';
|
||||
String get repeater_neighbors => 'Neighbors';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'View zero hop neighbors.';
|
||||
String get repeater_neighborsSubtitle => 'View zero hop neighbors.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Settings';
|
||||
@@ -2287,10 +2502,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_receivedData => 'Received Neighbours Data';
|
||||
String get neighbors_receivedData => 'Received Neighbors Data';
|
||||
|
||||
@override
|
||||
String get neighbors_requestTimedOut => 'Neighbours request timed out.';
|
||||
String get neighbors_requestTimedOut => 'Neighbors request timed out.';
|
||||
|
||||
@override
|
||||
String neighbors_errorLoading(String error) {
|
||||
@@ -2298,10 +2513,10 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repeaters Neighbours';
|
||||
String get neighbors_repeatersNeighbors => 'Repeaters Neighbors';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'No neighbours data available.';
|
||||
String get neighbors_noData => 'No neighbors data available.';
|
||||
|
||||
@override
|
||||
String neighbors_unknownContact(String pubkey) {
|
||||
@@ -2604,6 +2819,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'All';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Add to favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Remove from favorites';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Users';
|
||||
|
||||
@@ -2618,4 +2842,406 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'New group';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'You';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Path trace failed.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Path trace not available.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Refresh Path Trace.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'One or more of the hops is missing a location!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Clear path.';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Select start and end nodes for LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Line-of-sight check failed: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Clear all points';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Run LOS to view elevation profile';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS Menu';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle => 'Tap nodes or long-press map for custom points';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Show display nodes';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Custom points';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Custom $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Point A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Point B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenna A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenna B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Run LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'No elevation data';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, clear LOS, min clearance $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, blocked by $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: checking...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: no data';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total clear, $blocked blocked, $unknown unknown';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Elevation data unavailable for one or more samples.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Invalid points/elevation data for LOS calculation.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Rename custom point';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Point name';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Show LOS panel';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Hide LOS panel';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Elevation data: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radio horizon';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'LOS beam';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrain';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequency';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'View calculation details';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Radio horizon calculation';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Starting from k=$baselineK at $baselineFreq MHz, the calculation adjusts the k-factor for the current $frequencyMHz MHz band, which defines the curved radio horizon cap.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Path Trace';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Ping';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Path trace to repeater';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Ping repeater';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Path trace to room server';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping room server';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Path trace route';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Trace route to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Clipboard is empty.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Invalid contact data';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Contact has been imported.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed => 'Failed to import contact.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Zero Hop Advert';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Flood Advert';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Copy Advert to Clipboard';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard => 'Add Contact from Clipboard';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Copy contact to Clipboard';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Share contact by advert';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Sent contact by advert.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed => 'Failed to send contact.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied => 'Advert copied to Clipboard.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Copying advert to Clipboard failed.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'MeshCore Activity';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'messages',
|
||||
one: 'message',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'channel messages',
|
||||
one: 'channel message',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'new nodes',
|
||||
one: 'new node',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'New $contactType discovered';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Received new message';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Export repeaters / room server to GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exports repeaters / roomserver with a location to GPX file.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Export companions to GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exports companions with a location to GPX file.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Export all contacts to GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exports all contacts with a location to GPX file.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Successfully exported GPX file.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'No contacts to export.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Not supported on your device/OS';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'There was an error when exporting.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Repeater & room server locations';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Companion locations';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'All contacts locations';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Map data exported from meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open GPX map data export';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nearby Repeaters';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Last seen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Contacts settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatic Discovery';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle => 'Other contact related settings';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle => 'Auto-add users';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Allow the companion to automatically add discovered users.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle => 'Auto-add repeaters';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Allow the companion to automatically add discovered repeaters.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Auto-add room servers';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Allow the companion to automatically add discovered room servers.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle => 'Auto-add sensors';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Allow the companion to automatically add discovered sensors.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overwrite Oldest';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'When the contact list is full, the oldest non-favorited contact will be replaced.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Discovered Contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'No matching contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Search discovered contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact added';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Add Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copy Contact to clipboard';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Delete Discovered Contact';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Delete All Discovered Contacts';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Are you sure you want to delete all discovered contacts?';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Eliminar todo';
|
||||
|
||||
@override
|
||||
String get common_close => 'Cerrar';
|
||||
|
||||
@@ -108,6 +111,91 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Conecte mediante USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Seleccione el dispositivo de serie detectado y conéctelo directamente a su nodo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Seleccione un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La comunicación serial a través de USB está activa en dispositivos Android compatibles y en plataformas de escritorio.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'No se encontraron dispositivos USB. Conecte uno y vuelva a intentar.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Se denegó el permiso de acceso a través de USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'El dispositivo USB seleccionado ya no está disponible.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Seleccione un dispositivo USB válido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Ya se ha iniciado una solicitud de conexión USB adicional.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'No hay ningún dispositivo USB conectado.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'No se pudo abrir el dispositivo USB seleccionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'No se pudo conectar con el dispositivo USB seleccionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'La comunicación serial a través de USB no está soportada en esta plataforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'La conexión USB ya está activa.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'No se ha seleccionado ningún dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'La conexión USB no está activa.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'La conexión ha caducado. Asegúrese de que el dispositivo tenga el firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Dispositivo de serie web';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Seleccione un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Conectándose al dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Buscando dispositivos USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Error al conectar mediante USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Escaneando dispositivos...';
|
||||
|
||||
@@ -143,6 +231,23 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Escanea';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth está desactivado.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Por favor, active el Bluetooth para escanear dispositivos.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Navegador Chrome requerido';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Esta aplicación web requiere Google Chrome o un navegador basado en Chromium para el soporte de Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Habilitar Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Cambiar rápidamente';
|
||||
|
||||
@@ -223,6 +328,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Longitud';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Configuración de contacto';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Configuración de cómo se agregan los contactos.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Modo Privacidad';
|
||||
|
||||
@@ -313,6 +425,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Un cliente de código abierto de Flutter para dispositivos de red mesh LoRa de MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Datos de elevación LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Nombre';
|
||||
|
||||
@@ -337,15 +453,6 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preajustes';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frecuencia (MHz)';
|
||||
|
||||
@@ -374,10 +481,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potencia de TX inválida (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Largo Alcance';
|
||||
String get settings_clientRepeat => 'Repetir sin conexión';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocidad Rápida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permita que este dispositivo repita los paquetes de red para otros usuarios.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Para la comunicación fuera de la red, se requiere una frecuencia de 433, 869 o 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -447,6 +559,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Ruso';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Habilitar seguimiento de mensajes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Mostrar metadatos detallados de enrutamiento y tiempo para los mensajes';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notificaciones';
|
||||
|
||||
@@ -608,6 +734,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Caché de Mapa Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Unidades';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Métrico (m/km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperial (pies/millas)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'No se ha seleccionado ningún área';
|
||||
|
||||
@@ -645,7 +780,35 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
'Los contactos aparecerán cuando los dispositivos anuncien.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Buscar contactos...';
|
||||
String get contacts_unread => 'No leído';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Buscar contactos...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Buscar contactos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Buscar $number$str Favoritos...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Buscar $number$str Usuarios...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Buscar $number$str Repetidores...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Buscar $number$str servidores de sala...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'No contactos sin leer';
|
||||
@@ -714,23 +877,23 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String contacts_lastSeenMinsAgo(int minutes) {
|
||||
return 'Última vez visto hace $minutes minutos.';
|
||||
return '~ $minutes min.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenHourAgo => 'Última vez que se vio hace 1 hora';
|
||||
String get contacts_lastSeenHourAgo => '~ 1 hora';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenHoursAgo(int hours) {
|
||||
return 'Última vez visto hace $hours horas.';
|
||||
return '~ $hours horas';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_lastSeenDayAgo => 'Última vez que se vio hace 1 día';
|
||||
String get contacts_lastSeenDayAgo => '~ 1 día';
|
||||
|
||||
@override
|
||||
String contacts_lastSeenDaysAgo(int days) {
|
||||
return 'Última vez visto hace $days días.';
|
||||
return '~ $days días';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -771,6 +934,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Editar canal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenciar canal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Activar canal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Eliminar canal';
|
||||
|
||||
@@ -779,6 +948,11 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Eliminar \"$name\"? Esto no se puede deshacer.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'No se pudo eliminar el canal \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canal \"$name\" eliminado';
|
||||
@@ -1067,6 +1241,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestión de Rutas';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos los caminos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de enrutamiento';
|
||||
|
||||
@@ -1204,9 +1381,33 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Sin leer: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => '¿Abrir enlace?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'¿Quiere abrir este enlace en su navegador?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Abrir';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'No se pudo abrir el enlace: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Formato de enlace no válido';
|
||||
|
||||
@override
|
||||
String get map_title => 'Mapa de Nodos';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'No hay nodos con datos de ubicación';
|
||||
|
||||
@@ -1324,6 +1525,13 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostrar marcadores compartidos';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Mostrar las ubicaciones estimadas de los nodos.';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Ubicación estimada';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Última vez que se vio';
|
||||
|
||||
@@ -1336,6 +1544,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gestionar Repetidor';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Pulse en los nodos para agregarlos al camino.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Ejecutar Rastreo de Ruta';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Eliminar último';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Rastreo de ruta cancelado.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Caché de Mapa Offline';
|
||||
|
||||
@@ -1632,10 +1852,10 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Enviar comandos al repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vecinos';
|
||||
String get repeater_neighbors => 'Vecinos';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Ver vecinos de salto cero.';
|
||||
String get repeater_neighborsSubtitle => 'Ver vecinos de salto cero.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Configuración';
|
||||
@@ -2334,7 +2554,7 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repetidores Vecinos';
|
||||
String get neighbors_repeatersNeighbors => 'Repetidores Vecinos';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'No hay datos de vecinos disponibles.';
|
||||
@@ -2565,32 +2785,32 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Regenerar Contraseña Secreta';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regenerar la clave secreta para \"$name\"? Todos los miembros deberán escanear el nuevo código QR para seguir comunicándose.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Regenerar';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Código secreto regenerado para \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Actualizar Contraseña';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Confidencialidad actualizada para \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Escanear el nuevo código QR para actualizar el secreto de \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2643,6 +2863,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Todas';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Añadir a favoritos';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Eliminar de las favoritas';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Usuarios';
|
||||
|
||||
@@ -2657,4 +2886,421 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Nuevo grupo';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Tú';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'El trazado de ruta falló.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'El trazado de ruta no está disponible.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Actualizar Path Trace';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Uno o más de los lúpulos carecen de una ubicación';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Borrar ruta';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Seleccione los nodos de inicio y fin para LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Error en la comprobación de la línea de visión: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Borrar todos los puntos';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Ejecute LOS para ver el perfil de elevación';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menú LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Toque nodos o mantenga presionado el mapa para puntos personalizados';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Mostrar nodos de visualización';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Puntos personalizados';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Personalizado $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punto A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punto B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Ejecutar LOS';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Sin datos de elevación';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, despejar LOS, autorización mínima $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, bloqueado por $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: comprobando...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: sin datos';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total claro, $blocked bloqueado, $unknown desconocido';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Datos de elevación no disponibles para una o más muestras.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Datos de puntos/elevación no válidos para el cálculo de LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint =>
|
||||
'Cambiar el nombre del punto personalizado';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nombre del punto';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Mostrar panel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Ocultar panel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Datos de elevación: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horizonte radioeléctrico';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Línea de visión';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terreno';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frecuencia';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Ver detalles del cálculo';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Cálculo del horizonte radioeléctrico';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'A partir de k=$baselineK en $baselineFreq MHz, el cálculo ajusta el factor k para la banda actual de $frequencyMHz MHz, que define el límite curvo del horizonte de radio.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Rastreo de caminos';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Ping';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Rastrear ruta al repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Pingar repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Rastreo de ruta al servidor de la habitación';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pingar servidor de sala';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Ruta de trazado';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Rastrear ruta a $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'El portapapeles está vacío.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Datos de contacto no válidos';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'El contacto ha sido importado.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Contacto no se importó correctamente.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Anuncio de Zero Hop';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Anuncio de inundación';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Copiar anuncio al portapapeles';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Agregar contacto desde el portapapeles';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Copiar contacto al Portapapeles';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Compartir contacto por anuncio';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Envió contacto por anuncio.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'No se pudo enviar el contacto.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied => 'Anuncio copiado al Portapapeles.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Copiar anuncio al Portapapeles ha fallado.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Actividad de MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'mensajes',
|
||||
one: 'mensaje',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'mensajes de canal',
|
||||
one: 'mensaje de canal',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nuevos nodos',
|
||||
one: 'nuevo nodo',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Nuevo $contactType descubierto';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Nuevo mensaje recibido';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exportar repetidores / servidor de sala a GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exporta repetidores o roomserver con una ubicación a un archivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Exportar compañeros a GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exporta compañeros con una ubicación a archivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Exportar todos los contactos a GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exporta todos los contactos con una ubicación a un archivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Archivo GPX exportado con éxito.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'No hay contactos para exportar.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'No compatible con tu dispositivo/SO';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Hubo un error al exportar.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Ubicaciones del servidor de repetidor y sala';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Ubicaciones de compañero';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts =>
|
||||
'Todas las ubicaciones de contactos';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Datos del mapa exportados desde meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open exportación de datos de mapa GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Repetidores cercanos';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Visto por última vez';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Configuración de contactos';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Detección automática';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Otras configuraciones relacionadas con el contacto';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Agregar usuarios automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente a los usuarios descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Agregar repetidores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los repetidores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Agregar automáticamente servidores de sala';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los servidores de salas descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Agregar sensores automáticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Permitir que el compañero agregue automáticamente los sensores descubiertos.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sobreescribir el más antiguo';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Cuando la lista de contactos esté llena, se reemplazará el contacto no favorito más antiguo.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching =>
|
||||
'No se encontraron contactos coincidentes';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Buscar contactos descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contacto agregado';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Agregar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact =>
|
||||
'Copiar contacto al portapapeles';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Eliminar contacto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminar Todos los Contactos Descubiertos';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'¿Está seguro de que desea eliminar todos los contactos descubiertos!';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Elimina';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Elimina tutto';
|
||||
|
||||
@override
|
||||
String get common_close => 'Chiudi';
|
||||
|
||||
@@ -108,6 +111,92 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Connessione tramite USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Seleziona il dispositivo seriale rilevato e connettilo direttamente al tuo nodo MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Seleziona un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'La comunicazione seriale USB è attiva sui dispositivi Android supportati e sulle piattaforme desktop.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nessun dispositivo USB rilevato. Collegare uno e aggiornare.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'È stato negato l\'accesso tramite USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Il dispositivo USB selezionato non è più disponibile.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Seleziona un dispositivo USB valido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Un\'altra richiesta di connessione tramite USB è già in corso.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Non è collegato alcun dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Impossibile aprire il dispositivo USB selezionato.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Impossibile connettersi al dispositivo USB selezionato.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'La comunicazione seriale tramite USB non è supportata su questa piattaforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'La connessione USB è già attiva.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Non è stato selezionato alcun dispositivo USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'La connessione USB non è attiva.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'La connessione è scaduta. Assicurarsi che il dispositivo abbia il firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Dispositivo per comunicazione seriale su rete';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Seleziona un dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Connessione al dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Ricerca di dispositivi USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Errore nella connessione USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scansione in corso per i dispositivi...';
|
||||
|
||||
@@ -143,6 +232,23 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scansiona';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Il Bluetooth è disattivato.';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'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
|
||||
String get scanner_enableBluetooth => 'Abilita il Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Passa velocemente';
|
||||
|
||||
@@ -223,6 +329,13 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get settings_privacyMode => 'Modalità Privacy';
|
||||
|
||||
@@ -312,6 +425,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'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
|
||||
String get settings_infoName => 'Nome';
|
||||
|
||||
@@ -336,15 +453,6 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preset';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequenza (MHz)';
|
||||
|
||||
@@ -373,10 +481,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potere TX non valido (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lungo Raggio';
|
||||
String get settings_clientRepeat => 'Ripetizione \"fuori dalla rete\"';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocità Rapida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permetti a questo dispositivo di ripetere i pacchetti di rete per gli altri.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Per la comunicazione fuori rete, è necessario utilizzare frequenze di 433, 869 o 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -446,6 +559,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Russo';
|
||||
|
||||
@override
|
||||
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
|
||||
String get appSettings_notifications => 'Notifiche';
|
||||
|
||||
@@ -607,6 +734,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get appSettings_noAreaSelected => 'Nessun\'area selezionata';
|
||||
|
||||
@@ -644,7 +780,35 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
'I contatti appariranno quando i dispositivi pubblicizzano.';
|
||||
|
||||
@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
|
||||
String get contacts_noUnreadContacts => 'Nessun contatto non letto';
|
||||
@@ -769,6 +933,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Modifica canale';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenzia canale';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Attiva notifiche canale';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Elimina canale';
|
||||
|
||||
@@ -777,6 +947,11 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Eliminare \"$name\"? Non può essere annullato.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Impossibile eliminare il canale \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canale \"$name\" eliminato';
|
||||
@@ -1065,6 +1240,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gestione Percorsi';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostra tutti i percorsi';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modalità di routing';
|
||||
|
||||
@@ -1203,9 +1381,33 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Non letti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Aprire il link?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Vuoi aprire questo link nel tuo browser?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Apri';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Impossibile aprire il link: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Formato di link non valido';
|
||||
|
||||
@override
|
||||
String get map_title => 'Mappa Nodi';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linea di vista';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linea di vista';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Nessun nodo con dati di posizione';
|
||||
|
||||
@@ -1323,6 +1525,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostra i segnaposto condivisi';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations => 'Mostra le posizioni stimate dei nodi';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Località indovinata';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Ultimo Tempo di Visualizzazione';
|
||||
|
||||
@@ -1335,6 +1543,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gestisci Ripetitore';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tocca i nodi per aggiungerli al percorso.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Esegui Path Trace';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Rimuovi ultimo';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Tracciamento del percorso annullato.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Cache Mappa Offline';
|
||||
|
||||
@@ -1630,10 +1850,10 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Invia comandi al ripetitore';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vicini';
|
||||
String get repeater_neighbors => 'Vicini';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Visualizza vicini di salto pari a zero.';
|
||||
|
||||
@override
|
||||
@@ -2334,7 +2554,7 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Ripetitori Vicini';
|
||||
String get neighbors_repeatersNeighbors => 'Ripetitori Vicini';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Nessun dato sugli vicini disponibile.';
|
||||
@@ -2565,32 +2785,32 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Ri genera la chiave segreta';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regenera la chiave segreta per \"$name\"? Tutti i membri dovranno scansionare il nuovo codice QR per continuare a comunicare.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Rigenera';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Codice segreto rigenerato per \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Aggiorna Segreto';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Segreto aggiornato per \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Scansiona il nuovo codice QR per aggiornare il segreto di \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2643,6 +2863,15 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get listFilter_users => 'Utenti';
|
||||
|
||||
@@ -2657,4 +2886,422 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Nuovo gruppo';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Tu';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Tracciamento del percorso fallito.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable =>
|
||||
'Tracciamento del percorso non disponibile.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Aggiorna Path Trace.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Uno o più dei luppoli mancano di una posizione!';
|
||||
|
||||
@override
|
||||
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
|
||||
String get contacts_pathTrace => 'Traccia Percorso';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Ping';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Traccia percorso al ripetitore';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Ripetitore ping';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Traccia del percorso al server della stanza';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping al server della stanza';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Traccia percorso path';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Traccia percorso verso $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'La clipboard è vuota.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Dati di contatto non validi';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Il contatto è stato importato.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Contatto non importato con successo.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Annuncio Zero Hop';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Annuncio alluvionale';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Copia Annuncio negli Appunti';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Aggiungere contatto dalla clipboard';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Copia contatto negli Appunti';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop =>
|
||||
'Condividi contatto tramite annuncio';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent =>
|
||||
'Inviato contatto tramite annuncio.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Invio del contatto non riuscito.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied => 'Annuncio copiato negli Appunti.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Copia dell\'annuncio nella Clipboard non riuscita.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Attività MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'messaggi',
|
||||
one: 'messaggio',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'messaggi del canale',
|
||||
one: 'messaggio del canale',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nuovi nodi',
|
||||
one: 'nuovo nodo',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Nuovo $contactType scoperto';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Nuovo messaggio ricevuto';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Esporta ripetitori / server di stanza in GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Esporta ripetitori / roomserver con una posizione in un file GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Esporta compagni in GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Esporta i compagni con una posizione in un file GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Esporta tutti i contatti in GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Esporta tutti i contatti con una posizione in un file GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess =>
|
||||
'Esportazione del file GPX completata con successo.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Nessun contatto da esportare.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Non supportato sul tuo dispositivo/Sistema Operativo';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError =>
|
||||
'Si è verificato un errore durante l\'esportazione.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Posizioni del server ripetitore e della stanza';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Posizioni dei compagni';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Tutte le posizioni dei contatti';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Dati mappa esportati da meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open esportazione dati mappa GPX';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Ripetitori vicini';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ultimo accesso';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Impostazioni dei contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Scoperta automatica';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Altre impostazioni relative ai contatti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Aggiungere utenti automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente gli utenti scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Aggiungere ripetitori automaticamente';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i ripetitori scoperti.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Aggiungere automaticamente i server delle stanze';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i server delle stanze scoperte.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Aggiungere automaticamente i sensori';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Consenti al compagno di aggiungere automaticamente i sensori scoperti';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle =>
|
||||
'Sostituisci il più vecchio';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Quando l\'elenco dei contatti è pieno, il contatto più vecchio non tra i preferiti verrà sostituito.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Nessun contatto corrispondente';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Cerca contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contatto aggiunto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Aggiungi contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Copia contatto negli appunti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Elimina Contatto';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Eliminare tutti i contatti scoperti';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Sei sicuro di voler eliminare tutti i contatti scoperti?';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Verwijderen';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Alles verwijderen';
|
||||
|
||||
@override
|
||||
String get common_close => 'Sluiten';
|
||||
|
||||
@@ -108,6 +111,89 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Verbind via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Selecteer een gedetecteerd seriële apparaat en verbind deze direct met uw MeshCore-node.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Selecteer een USB-apparaat';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-serieel is actief op ondersteunde Android-apparaten en desktop-platforms.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Geen USB-apparaten gevonden. Sluit er een aan en herlaad.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Toegang via USB is geweigerd.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Het geselecteerde USB-apparaat is niet meer beschikbaar.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Selecteer een geldig USB-apparaat.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Een andere verzoek om een USB-verbinding is al in behandeling.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Er is geen USB-apparaat aangesloten.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Kon het geselecteerde USB-apparaat niet openen.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Kon niet verbinding maken met het geselecteerde USB-apparaat.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB-serieel is niet ondersteund op deze platform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Een USB-verbinding is al actief.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Geen USB-apparaat is geselecteerd.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'De USB-verbinding is niet actief.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Verbinding is verbroken. Zorg ervoor dat het apparaat de juiste USB-firmware heeft.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web-serieapparaat';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Selecteer een USB-apparaat';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Verbinding maken met USB-apparaat...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Zoeken naar USB-apparaten...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Fout bij de USB-verbinding: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Scannen naar apparaten...';
|
||||
|
||||
@@ -142,6 +228,23 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Scan';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Zorg ervoor dat Bluetooth is ingeschakeld om naar apparaten te zoeken.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Chrome-browser vereist';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Deze webapplicatie vereist Google Chrome of een op Chromium gebaseerde browser voor Bluetooth-ondersteuning.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Activeer Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snelle overschakeling';
|
||||
|
||||
@@ -223,6 +326,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Lengtegraad';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Contactinstellingen';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Instellingen voor het toevoegen van contacten';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Privacy Mode';
|
||||
|
||||
@@ -310,6 +420,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Een open-source Flutter client voor MeshCore LoRa mesh netwerkapparaten.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'LOS-hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Naam';
|
||||
|
||||
@@ -334,15 +448,6 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequentie (MHz)';
|
||||
|
||||
@@ -371,10 +476,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ongeldige TX-vermogen (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lange Afstand';
|
||||
String get settings_clientRepeat => 'Herhalen: Afgekoppeld';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Hoge Snelheid';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -444,6 +554,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Russisch';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Gedetailleerde routerings- en timing-metadata voor berichten weergeven';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Notificaties';
|
||||
|
||||
@@ -605,6 +728,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaarten Cache';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Eenheden';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metrisch (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperiaal (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Geen gebied geselecteerd';
|
||||
|
||||
@@ -642,7 +774,35 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
'Contacten verschijnen wanneer apparaten zich aanbieden.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Zoek contacten...';
|
||||
String get contacts_unread => 'Ongelezen';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Zoek contacten...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Zoek contacten...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Zoek $number$str favorieten...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Zoek $number$str gebruikers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Zoek $number$str Repeaters...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Zoek $number$str Room servers...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Geen ongelezen contacten';
|
||||
@@ -767,6 +927,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Kanaal bewerken';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Kanaal dempen';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Kanaal dempen opheffen';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Kanaal verwijderen';
|
||||
|
||||
@@ -775,6 +941,11 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Verwijderen \"$name\"? Dit kan niet worden teruggedraaid.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kan kanaal $name niet verwijderen';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanaal \"$name\" verwijderd';
|
||||
@@ -1062,6 +1233,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Beheer van Paden';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Toon alle paden';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Routeerwijze';
|
||||
|
||||
@@ -1199,9 +1373,33 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Nieuw: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Link openen?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Wilt u deze link in uw browser openen?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Openen';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Kan link niet openen: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Ongeldig linkformaat';
|
||||
|
||||
@override
|
||||
String get map_title => 'Node Map';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Geen nodes met locatiegegevens';
|
||||
|
||||
@@ -1319,6 +1517,13 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Toon gedeelde markeringen';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Toon de voorspelde locaties van de knopen';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Geroerde locatie';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Laatste Bekeken Tijd';
|
||||
|
||||
@@ -1331,6 +1536,19 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Beheer Repeater';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd =>
|
||||
'Tik op knooppunten om ze toe te voegen aan het pad';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Padeshulp traceren';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Verwijder Laatste';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Pad traceren geannuleerd';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Kaarten Cache';
|
||||
|
||||
@@ -1625,10 +1843,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Verzend commando\'s naar de repeater';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Buren';
|
||||
String get repeater_neighbors => 'Buren';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Bekijk nul hops buren.';
|
||||
String get repeater_neighborsSubtitle => 'Bekijk nul hops buren.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Instellingen';
|
||||
@@ -2324,7 +2542,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Herhalingen Buren';
|
||||
String get neighbors_repeatersNeighbors => 'Herhalingen Buren';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Geen gegevens van buren beschikbaar.';
|
||||
@@ -2556,32 +2774,32 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Regeneer Geheimwoord';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regeneere de geheime sleutel voor \"$name\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Regeneer';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Geheim hersteld voor \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Bijwerken Geheime';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Geheim gewijzigd voor \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Scan de nieuwe QR-code om het geheim voor \"$name\" bij te werken';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2634,6 +2852,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Alles';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Toevoegen aan favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Verwijderen uit favorieten';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Gebruikers';
|
||||
|
||||
@@ -2648,4 +2875,417 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Nieuwe groep';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Jij';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Padtrace mislukt.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Padtrace niet beschikbaar.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Path Trace vernieuwen.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Een of meer van de hops ontbreken een locatie!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Weg wissen';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd =>
|
||||
'Selecteer begin- en eindknooppunten voor LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Zichtlijncontrole mislukt: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Wis alle punten';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Voer LOS uit om het hoogteprofiel te bekijken';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'LOS-menu';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Tik op knooppunten of druk lang op de kaart voor aangepaste punten';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Toon weergaveknooppunten';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Aangepaste punten';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Aangepast $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antenne A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antenne B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Voer LOS uit';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Geen hoogtegegevens';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, vrije LOS, min. vrije ruimte $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, geblokkeerd door $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: controleren...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: geen gegevens';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total gewist, $blocked geblokkeerd, $unknown onbekend';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Hoogtegegevens niet beschikbaar voor een of meer monsters.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Ongeldige punten/hoogtegegevens voor LOS-berekening.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Hernoem aangepast punt';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Puntnaam';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Toon LOS-paneel';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'LOS-paneel verbergen';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Hoogtegegevens: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Radiohorizon';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Zichtlijn';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Terrein';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Frequentie';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Bekijk details van de berekening';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Berekening van de radiohorizon';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Beginnend met k=$baselineK bij $baselineFreq MHz, wordt bij de berekening de k-factor aangepast voor de huidige $frequencyMHz MHz-band, die de gebogen radiohorizonkap definieert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Pad Traceren';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Pingen';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Pad traceren naar repeater';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Ping repeater';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Padtrace naar room server';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping kamer server';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Route traceren';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Trace route to $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Knipbord is leeg.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Ongeldige contactgegevens';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Contact is geïmporteerd.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Contact kon niet geïmporteerd worden.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Zero Hop Reclame';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Overstromingsadvertentie';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Advert naar klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Contact uit klembord toevoegen';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kontakt naar Klembord kopiëren';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Contact delen via advertentie';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent =>
|
||||
'Contact verzonden via advertentie';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Mislukt om contact te verzenden';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied =>
|
||||
'Reclame gekopieerd naar Klembord.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopiëren van advertentie naar Clipboard is mislukt.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'MeshCore Activiteit';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'berichten',
|
||||
one: 'bericht',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'kanaalberichten',
|
||||
one: 'kanaalbericht',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nieuwe knooppunten',
|
||||
one: 'nieuw knooppunt',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Nieuw $contactType ontdekt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Nieuw bericht ontvangen';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exporteer repeaters / roomserver naar GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exporteert repeaters / roomserver met een locatie naar GPX-bestand.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Companions exporteren naar GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exporteert metgezellen met een locatie naar een GPX-bestand.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Alle contacten exporteren naar GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exporteert alle contacten met een locatie naar een GPX-bestand.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Succesvol GPX-bestand geëxporteerd.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Geen contacten om te exporteren.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Niet ondersteund op uw apparaat/besturingssysteem';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Er was een fout bij het exporteren.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Repeater- en kamer servers locaties';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Locaties van metgezellen';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Alle contactlocaties';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Kaartgegevens geëxporteerd uit meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'meshcore-open GPX kaartgegevens exporteren';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nabije herhalingseenheden';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Laatst gezien';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Instellingen voor contacten';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatische detectie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Andere instellingen voor contactgerelateerde zaken';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Gebruikers automatisch toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte gebruikers toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatisch herhalingstoestellen toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte repeaters toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatisch kamerservers toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte kamer servers toevoegt.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatisch sensoren toevoegen';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Sta toe dat de companion automatisch ontdekte sensoren toevoegt';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Overschrijf Oudste';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Wanneer de contactenlijst vol is, wordt de oudste niet-favoriete contactpersoon vervangen.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Ontdekte contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Geen overeenkomende contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Ontdekte contacten zoeken';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Contact toegevoegd';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Contact toevoegen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopieer contact naar klembord';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Contact verwijderen';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Verwijder alle ontdekte contacten';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Weet u zeker dat u alle ontdekte contacten wilt verwijderen?';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Usuń';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Usuń wszystko';
|
||||
|
||||
@override
|
||||
String get common_close => 'Zamknąć';
|
||||
|
||||
@@ -108,6 +111,92 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Połącz przez USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Wybierz urządzenie USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Zostało odrzucone żądanie dostępu przez USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Wybór urządzenia USB już nie jest dostępny.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Wybierz prawidłowe urządzenie USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Brak podłączonego urządzenia USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Nie udało się otworzyć wybranego urządzenia USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Nie udało się połączyć z wybranym urządzeniem USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Port szeregowy USB nie jest obsługiwany na tym urządzeniu.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Połączenie USB jest już aktywne.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Nie został wybrany żaden urządzenie USB.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'Połączenie USB nie jest aktywne.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\".';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName =>
|
||||
'Urządzenie do komunikacji przez sieć (seria)';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Wybierz urządzenie USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Połączenie z urządzeniem USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Wyszukiwanie urządzeń USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Błąd połączenia USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skanowanie urządzeń...';
|
||||
|
||||
@@ -143,6 +232,23 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Przeskanuj';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth jest wyłączony';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'Prosimy włączyć Bluetooth, aby przeskanować urządzenia.';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequired => 'Wymagana przeglądarka Chrome';
|
||||
|
||||
@override
|
||||
String get scanner_chromeRequiredMessage =>
|
||||
'Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.';
|
||||
|
||||
@override
|
||||
String get scanner_enableBluetooth => 'Włącz Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Szybka zmiana';
|
||||
|
||||
@@ -225,6 +331,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Długość';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Ustawienia kontaktowe';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Ustawienia dotyczące sposobu dodawania kontaktów';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Tryb Prywatny';
|
||||
|
||||
@@ -313,6 +426,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'Otwarty kod źródłowy klient Flutter dla urządzeń do sieci mesh LoRa MeshCore.';
|
||||
|
||||
@override
|
||||
String get settings_aboutOpenMeteoAttribution =>
|
||||
'Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get settings_infoName => 'Imię';
|
||||
|
||||
@@ -337,15 +454,6 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Preset';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Częstotliwość (MHz)';
|
||||
|
||||
@@ -375,10 +483,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Nieprawidłowa moc TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Długi zasięg';
|
||||
String get settings_clientRepeat => 'Powtórzenie: Niezależne od sieci';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Szybka prędkość';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -448,6 +561,19 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Rosyjski';
|
||||
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukraińska';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracingSubtitle =>
|
||||
'Pokaż szczegółowe metadane trasowania i czasu dla wiadomości';
|
||||
|
||||
@override
|
||||
String get appSettings_notifications => 'Powiadomienia';
|
||||
|
||||
@@ -609,6 +735,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Bufor Map Offline';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsTitle => 'Jednostki';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsMetric => 'Metryczne (m / km)';
|
||||
|
||||
@override
|
||||
String get appSettings_unitsImperial => 'Imperialne (ft / mi)';
|
||||
|
||||
@override
|
||||
String get appSettings_noAreaSelected => 'Nie zaznaczono żadnej powierzchni.';
|
||||
|
||||
@@ -646,7 +781,35 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
'Kontakty będą wyświetlane, gdy urządzenia reklamują się.';
|
||||
|
||||
@override
|
||||
String get contacts_searchContacts => 'Wyszukaj kontakty...';
|
||||
String get contacts_unread => 'Nieprzeczytane';
|
||||
|
||||
@override
|
||||
String get contacts_searchContactsNoNumber => 'Wyszukaj kontakty...';
|
||||
|
||||
@override
|
||||
String contacts_searchContacts(int number, String str) {
|
||||
return 'Wyszukaj kontakty...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchFavorites(int number, String str) {
|
||||
return 'Wyszukaj $number$str ulubione...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchUsers(int number, String str) {
|
||||
return 'Wyszukaj $number$str Użytkowników...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRepeaters(int number, String str) {
|
||||
return 'Wyszukaj $number$str powtórników...';
|
||||
}
|
||||
|
||||
@override
|
||||
String contacts_searchRoomServers(int number, String str) {
|
||||
return 'Wyszukaj $number$str serwerów Room...';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_noUnreadContacts => 'Brak nieprzeczytanych kontaktów';
|
||||
@@ -772,6 +935,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Edytuj kanał';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Wycisz kanał';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Wyłącz wyciszenie kanału';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Usuń kanał';
|
||||
|
||||
@@ -780,6 +949,11 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Usuń \"$name\"? Nie można tego cofnąć.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Nie udało się usunąć kanału \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanał \"$name\" usunięto';
|
||||
@@ -1067,6 +1241,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Zarządzanie ścieżkami';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Pokaż wszystkie ścieżki';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Tryb routingu';
|
||||
|
||||
@@ -1205,9 +1382,33 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Niezgłoszone: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otworzyć link?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Czy chcesz otworzyć ten link w przeglądarce?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Otwórz';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Nie można otworzyć linku: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Nieprawidłowy format linku';
|
||||
|
||||
@override
|
||||
String get map_title => 'Mapa węzłów';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linia wzroku';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Linia wzroku';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Brak węzłów z danymi lokalizacyjnymi';
|
||||
|
||||
@@ -1325,6 +1526,13 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Pokaż współdzielone znaki.';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Wyświetl lokalizacje zgadanych węzłów';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Wydana lokalizacja';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Ostatni raz widiany';
|
||||
|
||||
@@ -1337,6 +1545,18 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Zarządzaj Powtórzami';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Kliknij na węzły, aby dodać je do ścieżki.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Uruchom ślad ścieżki';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Usuń ostatni';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Śledzenie ścieżki anulowano.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Bufor Map Offline';
|
||||
|
||||
@@ -1634,10 +1854,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Wyślij polecenia do powielacza';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Sąsiedzi';
|
||||
String get repeater_neighbors => 'Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
String get repeater_neighborsSubtitle =>
|
||||
'Wyświetl sąsiedztwo zerowych hopów.';
|
||||
|
||||
@override
|
||||
@@ -2333,7 +2553,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Powtarzacze Sąsiedzi';
|
||||
String get neighbors_repeatersNeighbors => 'Powtarzacze Sąsiedzi';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Brak danych dotyczących sąsiadów.';
|
||||
@@ -2564,32 +2784,32 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Zregeneruj sekret';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regeneruj tajny klucz dla \"$name\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Zregeneruj';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Hasło ponownie wygenerowane dla \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Zaktualizuj tajny klucz';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Hasło zaktualizowane dla \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Skanuj nowy kod QR, aby zaktualizować sekret dla \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2642,6 +2862,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get listFilter_all => 'Wszystko';
|
||||
|
||||
@override
|
||||
String get listFilter_favorites => 'Ulubione';
|
||||
|
||||
@override
|
||||
String get listFilter_addToFavorites => 'Dodaj do ulubionych';
|
||||
|
||||
@override
|
||||
String get listFilter_removeFromFavorites => 'Usuń z ulubionych';
|
||||
|
||||
@override
|
||||
String get listFilter_users => 'Użytkownicy';
|
||||
|
||||
@@ -2656,4 +2885,423 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Nowa grupa';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Ty';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Śledzenie ścieżki nie powiodło się.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Ścieżka śledzenia niedostępna.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Odśwież ścieżkę.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Jeden lub więcej z chmieli nie ma określonej lokalizacji!';
|
||||
|
||||
@override
|
||||
String get pathTrace_clearTooltip => 'Wyczyść ścieżkę';
|
||||
|
||||
@override
|
||||
String get losSelectStartEnd => 'Wybierz węzły początkowe i końcowe dla LOS.';
|
||||
|
||||
@override
|
||||
String losRunFailed(String error) {
|
||||
return 'Sprawdzenie pola widzenia nie powiodło się: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losClearAllPoints => 'Wyczyść wszystkie punkty';
|
||||
|
||||
@override
|
||||
String get losRunToViewElevationProfile =>
|
||||
'Uruchom LOS, aby wyświetlić profil wysokości';
|
||||
|
||||
@override
|
||||
String get losMenuTitle => 'Menu LOS';
|
||||
|
||||
@override
|
||||
String get losMenuSubtitle =>
|
||||
'Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty';
|
||||
|
||||
@override
|
||||
String get losShowDisplayNodes => 'Pokaż węzły wyświetlające';
|
||||
|
||||
@override
|
||||
String get losCustomPoints => 'Punkty niestandardowe';
|
||||
|
||||
@override
|
||||
String losCustomPointLabel(int index) {
|
||||
return 'Niestandardowe $index';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losPointA => 'Punkt A';
|
||||
|
||||
@override
|
||||
String get losPointB => 'Punkt B';
|
||||
|
||||
@override
|
||||
String losAntennaA(String value, String unit) {
|
||||
return 'Antena A: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losAntennaB(String value, String unit) {
|
||||
return 'Antena B: $value $unit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losRun => 'Uruchom LOS-a';
|
||||
|
||||
@override
|
||||
String get losNoElevationData => 'Brak danych o wysokości';
|
||||
|
||||
@override
|
||||
String losProfileClear(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String clearance,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, czysty LOS, minimalny prześwit $clearance $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String losProfileBlocked(
|
||||
String distance,
|
||||
String distanceUnit,
|
||||
String obstruction,
|
||||
String heightUnit,
|
||||
) {
|
||||
return '$distance $distanceUnit, zablokowane przez $obstruction $heightUnit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losStatusChecking => 'LOS: sprawdzam...';
|
||||
|
||||
@override
|
||||
String get losStatusNoData => 'LOS: brak danych';
|
||||
|
||||
@override
|
||||
String losStatusSummary(int clear, int total, int blocked, int unknown) {
|
||||
return 'LOS: $clear/$total jasne, $blocked zablokowane, $unknown nieznane';
|
||||
}
|
||||
|
||||
@override
|
||||
String get losErrorElevationUnavailable =>
|
||||
'Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.';
|
||||
|
||||
@override
|
||||
String get losErrorInvalidInput =>
|
||||
'Nieprawidłowe dane punktów/wysokości do obliczenia LOS.';
|
||||
|
||||
@override
|
||||
String get losRenameCustomPoint => 'Zmień nazwę punktu niestandardowego';
|
||||
|
||||
@override
|
||||
String get losPointName => 'Nazwa punktu';
|
||||
|
||||
@override
|
||||
String get losShowPanelTooltip => 'Pokaż panel LOS';
|
||||
|
||||
@override
|
||||
String get losHidePanelTooltip => 'Ukryj panel LOS';
|
||||
|
||||
@override
|
||||
String get losElevationAttribution =>
|
||||
'Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)';
|
||||
|
||||
@override
|
||||
String get losLegendRadioHorizon => 'Horyzont radiowy';
|
||||
|
||||
@override
|
||||
String get losLegendLosBeam => 'Linia widoczności';
|
||||
|
||||
@override
|
||||
String get losLegendTerrain => 'Teren';
|
||||
|
||||
@override
|
||||
String get losFrequencyLabel => 'Częstotliwość';
|
||||
|
||||
@override
|
||||
String get losFrequencyInfoTooltip => 'Zobacz szczegóły obliczenia';
|
||||
|
||||
@override
|
||||
String get losFrequencyDialogTitle => 'Obliczanie horyzontu radiowego';
|
||||
|
||||
@override
|
||||
String losFrequencyDialogDescription(
|
||||
double baselineK,
|
||||
double baselineFreq,
|
||||
double frequencyMHz,
|
||||
double kFactor,
|
||||
) {
|
||||
return 'Zaczynając od k=$baselineK przy $baselineFreq MHz, obliczenia korygują współczynnik k dla bieżącego pasma $frequencyMHz MHz, które definiuje zakrzywiony limit horyzontu radiowego.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_pathTrace => 'Śledzenie Ścieżek';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Pingować';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Śledzenie ścieżki do repeatera';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Repeater pingowy';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace =>
|
||||
'Śledzenie ścieżki do serwera pokojowego';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pinguj serwer pokoju';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Śledź trasę promienia';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Śledź trasę do $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Schowek jest pusty.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Nieprawidłowe dane kontaktowe';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Kontakt został zaimportowany.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Kontakt nie został zaimportowany.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Reklama Zero Hop';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Reklama powodziowa';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Kopiuj ogłoszenie do schowka';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard => 'Dodaj kontakt z schowka';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kopiuj kontakt do schowka';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop =>
|
||||
'Udostępnij kontakt przez ogłoszenie';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent =>
|
||||
'Wysłano kontakt przez ogłoszenie.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Nie udało się wysłać kontaktu.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied => 'Reklama skopiowana do schowka.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopiowanie ogłoszenia do schowka nie powiodło się.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Aktywność MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'wiadomości',
|
||||
many: 'wiadomości',
|
||||
few: 'wiadomości',
|
||||
one: 'wiadomość',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'wiadomości kanału',
|
||||
many: 'wiadomości kanału',
|
||||
few: 'wiadomości kanału',
|
||||
one: 'wiadomość kanału',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nowych węzłów',
|
||||
many: 'nowych węzłów',
|
||||
few: 'nowe węzły',
|
||||
one: 'nowy węzeł',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Nowy $contactType wykryty';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Otrzymano nową wiadomość';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Eksportuj powtórki / serwer pokojowy do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Eksportuj towarzyszy do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Eksportuje towarzyszy z lokalizacją do pliku GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Eksportuj wszystkie kontakty do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Pomyślnie wyeksportowano plik GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts =>
|
||||
'Brak kontaktów do wyeksportowania.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Wystąpił błąd podczas eksportowania.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Lokalizacje serwerów powtarzających i pomieszczeń';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Lokalizacje towarzyszy';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Wszystkie lokalizacje kontaktów';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Dane mapy wyeksportowane z meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'Eksport danych mapy GPX meshcore-open';
|
||||
|
||||
@override
|
||||
String get snrIndicator_nearByRepeaters => 'Nadajniki w pobliżu';
|
||||
|
||||
@override
|
||||
String get snrIndicator_lastSeen => 'Ostatnio widziany';
|
||||
|
||||
@override
|
||||
String get contactsSettings_title => 'Ustawienia kontaktów';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddTitle => 'Automatyczne odnajdywanie';
|
||||
|
||||
@override
|
||||
String get contactsSettings_otherTitle =>
|
||||
'Inne ustawienia związane z kontaktami';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersTitle =>
|
||||
'Automatycznie dodaj użytkowników';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddUsersSubtitle =>
|
||||
'Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersTitle =>
|
||||
'Automatyczne dodawanie powtarzalników';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRepeatersSubtitle =>
|
||||
'Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersTitle =>
|
||||
'Automatycznie dodaj serwery pokojowe';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddRoomServersSubtitle =>
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsTitle =>
|
||||
'Automatycznie dodaj czujniki';
|
||||
|
||||
@override
|
||||
String get contactsSettings_autoAddSensorsSubtitle =>
|
||||
'Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestTitle => 'Nadpisz najstarszy';
|
||||
|
||||
@override
|
||||
String get contactsSettings_overwriteOldestSubtitle =>
|
||||
'Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_Title => 'Odkryte Kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_noMatching => 'Brak pasujących kontaktów';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_searchHint => 'Wyszukaj odkryte kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_contactAdded => 'Kontakt dodany';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_addContact => 'Dodaj kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_copyContact => 'Kopiuj kontakt do schowka';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContact => 'Usuń kontakt';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAll =>
|
||||
'Usuń wszystkie odkryte kontakty';
|
||||
|
||||
@override
|
||||
String get discoveredContacts_deleteContactAllContent =>
|
||||
'Czy na pewno chcesz usunąć wszystkie znalezione kontakty?';
|
||||
}
|
||||
|
||||
@@ -38,6 +38,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Excluir';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Excluir Tudo';
|
||||
|
||||
@override
|
||||
String get common_close => 'Fechar';
|
||||
|
||||
@@ -108,6 +111,91 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Conecte via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Selecione um dispositivo serial detectado e conecte-o diretamente ao seu nó MeshCore.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Selecione um dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'A comunicação serial via USB está ativa em dispositivos Android e plataformas de desktop compatíveis.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenhum dispositivo USB encontrado. Conecte um e atualize.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'A permissão para acesso via USB foi negada.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'O dispositivo USB selecionado não está mais disponível.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Selecione um dispositivo USB válido.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Já existe uma solicitação de conexão USB em andamento.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Não há nenhum dispositivo USB conectado.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Não foi possível abrir o dispositivo USB selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Não foi possível conectar ao dispositivo USB selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'A comunicação serial via USB não é suportada nesta plataforma.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'A conexão USB já está ativa.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Nenhum dispositivo USB foi selecionado.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'A conexão USB não está ativa.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'A conexão expirou. Verifique se o dispositivo possui o firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Dispositivo de Serial para a Web';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Selecione um dispositivo USB';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Conectando ao dispositivo USB...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Procurando por dispositivos USB...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Falha na conexão USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Procurando por dispositivos...';
|
||||
|
||||
@@ -143,6 +231,23 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Digitalizar';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth está desativado';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'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
|
||||
String get scanner_enableBluetooth => 'Ative o Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Mudar rapidamente';
|
||||
|
||||
@@ -224,6 +329,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get settings_privacyMode => 'Modo de Privacidade';
|
||||
|
||||
@@ -314,6 +426,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'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
|
||||
String get settings_infoName => 'Nome';
|
||||
|
||||
@@ -338,15 +454,6 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frequência (MHz)';
|
||||
|
||||
@@ -375,10 +482,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Potência de TX inválida (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Alcance Longo';
|
||||
String get settings_clientRepeat => 'Repetição sem rede';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Velocidade Rápida';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Permita que este dispositivo repita pacotes de rede para outros dispositivos.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'A repetição fora da rede requer frequências de 433, 869 ou 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -448,6 +560,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Russo';
|
||||
|
||||
@override
|
||||
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
|
||||
String get appSettings_notifications => 'Notificações';
|
||||
|
||||
@@ -608,6 +734,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get appSettings_noAreaSelected => 'Nenhuma área selecionada';
|
||||
|
||||
@@ -646,7 +781,35 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
'Os contatos serão exibidos quando os dispositivos anunciarem.';
|
||||
|
||||
@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
|
||||
String get contacts_noUnreadContacts => 'Sem contatos não lidos.';
|
||||
@@ -772,6 +935,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_editChannel => 'Editar canal';
|
||||
|
||||
@override
|
||||
String get channels_muteChannel => 'Silenciar canal';
|
||||
|
||||
@override
|
||||
String get channels_unmuteChannel => 'Ativar canal';
|
||||
|
||||
@override
|
||||
String get channels_deleteChannel => 'Excluir canal';
|
||||
|
||||
@@ -780,6 +949,11 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Excluir \"$name\"? Não pode ser desfeito.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Falha ao excluir o canal \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Canal \"$name\" excluído';
|
||||
@@ -1067,6 +1241,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Gerenciamento de Caminhos';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Mostrar todos os caminhos';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Modo de roteamento';
|
||||
|
||||
@@ -1204,9 +1381,33 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Não lido: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Abrir link?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Deseja abrir este link no seu navegador?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Abrir';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Não foi possível abrir o link: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Formato de link inválido';
|
||||
|
||||
@override
|
||||
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
|
||||
String get map_noNodesWithLocation =>
|
||||
'Não existem nós com dados de localização.';
|
||||
@@ -1325,6 +1526,13 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Mostrar marcadores compartilhados';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Mostrar as localizações dos nós estimados';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Localização estimada';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Último Tempo de Visualização';
|
||||
|
||||
@@ -1337,6 +1545,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Gerenciar Repetidor';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Toque nos nós para adicioná-los ao caminho.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Executar Traçado de Caminho';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Remover Último';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Rastreamento de caminho cancelado.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Cache de Mapa Offline';
|
||||
|
||||
@@ -1632,11 +1852,10 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Enviar comandos ao repetidor';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Vizinhos';
|
||||
String get repeater_neighbors => 'Vizinhos';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle =>
|
||||
'Visualizar vizinhos de salto zero.';
|
||||
String get repeater_neighborsSubtitle => 'Visualizar vizinhos de salto zero.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Configurações';
|
||||
@@ -2335,7 +2554,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Repetidores Vizinhos';
|
||||
String get neighbors_repeatersNeighbors => 'Repetidores Vizinhos';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Não estão disponíveis dados de vizinhos.';
|
||||
@@ -2567,32 +2786,32 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Regenerar Senha Segura';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regenerar a chave secreta para \"$name\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Regenerar';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Senha secreta regenerada para \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Atualizar Segredo';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Segredo atualizado para \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Scanar o novo código QR para atualizar o segredo para \"$name\"\n\n\n+++++';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2645,6 +2864,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get listFilter_users => 'Usuários';
|
||||
|
||||
@@ -2659,4 +2887,417 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Novo grupo';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Você';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Falha no rastreamento de caminho.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Traçado de caminho não disponível.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Atualizar Path Trace.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Um ou mais dos lúpulos estão sem localização!';
|
||||
|
||||
@override
|
||||
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
|
||||
String get contacts_pathTrace => 'Traçado de Caminho';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Pingar';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Traçar caminho para repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Pingar repetidor';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Traçar caminho para o servidor da sala';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Pingar servidor da sala';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Rastrear rota do caminho';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Rastrear rota para $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Área de Transferência Está Vazia.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Dados de Contato Inválidos';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Contato foi importado.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed => 'Contato falhou ao ser importado.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Anúncio Zero Hop';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Anúncio de Inundação';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard =>
|
||||
'Copiar Anúncio para Área de Transferência';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Adicionar Contato da Área de Transferência';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact =>
|
||||
'Copiar contato para Área de Transferência';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Compartilhar contato por anúncio';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Enviou contato por anúncio.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed => 'Falha ao enviar contato.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied =>
|
||||
'Anúncio copiado para a Área de Transferência.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Cópia do anúncio para a Área de Transferência falhou.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Atividade MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'mensagens',
|
||||
one: 'mensagem',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'mensagens de canal',
|
||||
one: 'mensagem de canal',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'novos nós',
|
||||
one: 'novo nó',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Novo $contactType descoberto';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Nova mensagem recebida';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exportar repetidores / servidor de sala para GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exporta repetidores / roomserver com localização para arquivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Exportar companheiros para GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exporta companheiros com uma localização para um arquivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Exportar todos os contatos para GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exporta todos os contatos com uma localização para um arquivo GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Arquivo GPX exportado com sucesso.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Nenhum contato para exportar.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Não suportado no seu dispositivo/SO';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Ocorreu um erro ao exportar.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Localizações do servidor de repetidor e sala';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Localizações de companheiros';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Todos os locais de contatos';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Dados do mapa exportados do meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'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?';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Odstrániť';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Zmazať všetko';
|
||||
|
||||
@override
|
||||
String get common_close => 'Zavrieť';
|
||||
|
||||
@@ -108,6 +111,91 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Pripojte cez USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Vyberte detekovaný sériový zariadenie a pripojte ho priamo k vašej MeshCore uzlu.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Vyberte USB zariadenie';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB sériová komunikácia je aktívna na podporovaných zariadeniach s Androidom a na desktopových platformách.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Nenašli sa žiadne USB zariadenia. Pripojte jedno a obnovte.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied =>
|
||||
'Žiadosť o prístup cez USB bola zamietnutá.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Vybrané USB zariadenie už nie je dostupné.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Vyberte platné USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'Ďalšia požiadavka na pripojenie cez USB je aktuálne v procese.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Nie je pripojené žiadne USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Nepodarilo sa otvoriť vybrané USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Nepodarilo sa sa sa pripojiť k vybranému USB zariadeniu.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'Podpora USB sériového rozhrania nie je na tejto platforme dostupná.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'Pripojenie cez USB je už aktivované.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected =>
|
||||
'Nebolo vybrané žiadne USB zariadenie.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'Pripojenie cez USB nie je aktivované.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Pripojenie nebolo úspešné. Uistite sa, že zariadenie má nainštalovaný firmware USB Companion.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Webový sériový zariadenie';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Vyberte USB zariadenie';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Pripojenie k USB zariadeniu...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Hľadanie USB zariadení...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Neúspešné pripojenie cez USB: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Skrívania zariadení...';
|
||||
|
||||
@@ -143,6 +231,23 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Skončiť';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth je vypnutý';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'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
|
||||
String get scanner_enableBluetooth => 'Povolte Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Rýchle prepínač';
|
||||
|
||||
@@ -223,6 +328,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_longitude => 'Dĺžka';
|
||||
|
||||
@override
|
||||
String get settings_contactSettings => 'Nastavenia kontaktov';
|
||||
|
||||
@override
|
||||
String get settings_contactSettingsSubtitle =>
|
||||
'Nastavenia pre pridávanie kontaktov.';
|
||||
|
||||
@override
|
||||
String get settings_privacyMode => 'Režim ochrany súkromia';
|
||||
|
||||
@@ -310,6 +422,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'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
|
||||
String get settings_infoName => 'Meno';
|
||||
|
||||
@@ -334,15 +450,6 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Prednastavenia';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frekvencia (MHz)';
|
||||
|
||||
@@ -371,10 +478,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Neplatná hodnota výkonu TX (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Dlhý dosah';
|
||||
String get settings_clientRepeat => 'Opätovné použitie bez elektrickej siete';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Rýchla rýchlosť';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Umožnite, aby toto zariadenie opakovávalo siete pre ostatných.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'Použitie off-grid systému vyžaduje frekvencie 433, 869 alebo 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -444,6 +556,19 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Ruština';
|
||||
|
||||
@override
|
||||
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
|
||||
String get appSettings_notifications => 'Upozornenia';
|
||||
|
||||
@@ -602,6 +727,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get appSettings_noAreaSelected => 'Neoznačila sa žiadna oblasť';
|
||||
|
||||
@@ -639,7 +773,35 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
'Kontakty sa zobrazia, keď zariadenia spúšťajú reklamu.';
|
||||
|
||||
@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
|
||||
String get contacts_noUnreadContacts => 'Žiadne neprečítané kontakty';
|
||||
@@ -767,6 +929,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get channels_deleteChannel => 'Odstrániť kanál';
|
||||
|
||||
@@ -775,6 +943,11 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Odstrániť \"$name\"? To sa nedá zrušiť.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Kanál \"$name\" sa nepodarilo odstrániť';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanál \"$name\" bol odstránený';
|
||||
@@ -1062,6 +1235,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Správa ciest';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Zobraziť všetky cesty';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Režim trasy';
|
||||
|
||||
@@ -1200,9 +1376,33 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Nezriadené: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Otvoriť odkaz?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Chcete otvoriť tento odkaz v prehliadači?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Otvoriť';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Nepodarilo sa otvoriť odkaz: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Neplatný formát odkazu';
|
||||
|
||||
@override
|
||||
String get map_title => 'Mapa uzlov';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Line of Sight';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Žiadne uzly s údajmi o polohe';
|
||||
|
||||
@@ -1320,6 +1520,13 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Zobraziť zdieľané značky';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Zobraziť umiestnenia odhadnutých uzlov';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Odhadnutá lokalita';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Posledný čas sledovania';
|
||||
|
||||
@@ -1332,6 +1539,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Spravovať Opakovanie';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Kliknite na uzly, aby ste ich pridali k ceste.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Spustiť trasovaním cesty';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Odstrániť posledný';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Zrušenie stopáže cesty bolo zrušené.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Mapa Pamäť';
|
||||
|
||||
@@ -1627,10 +1846,10 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Pošlite príkazy opakovaču';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Súsezný';
|
||||
String get repeater_neighbors => 'Súsezný';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||
String get repeater_neighborsSubtitle => 'Zobraziť susedné body bez skokov.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Nastavenia';
|
||||
@@ -2321,7 +2540,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Opakovadlá Súsezná';
|
||||
String get neighbors_repeatersNeighbors => 'Opakovadlá Súsezná';
|
||||
|
||||
@override
|
||||
String get neighbors_noData =>
|
||||
@@ -2553,32 +2772,32 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Zobraziť nový tajný kód';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Znovu vygenerovať tajný kľúč pre \"$name\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Znovu vygenerovať';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Záznam pre \"$name\" bol regenerovaný tajne';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Aktualizovať tajné heslo';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Zmena tajnej slova pre \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Skáňte nový QR kód na aktualizáciu tajného hesla pre \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2630,6 +2849,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get listFilter_users => 'Používatelia';
|
||||
|
||||
@@ -2644,4 +2872,417 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Nová skupina';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Vy';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Sledovanie cesty zlyhalo.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Path trace nie je k dispozícii.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Obnoviť Path Trace.';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'Jedna alebo viac chmeľov chýba lokalita!';
|
||||
|
||||
@override
|
||||
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
|
||||
String get contacts_pathTrace => 'Sledovanie lúčov';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Pingovať';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Sledovanie cesty k opakovaču';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Pingovať opakovač';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Sledovanie cesty k serveru miestnosti';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping server miestnosti';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Sledovať trasu lúča';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Sledovať trasu k $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Schránka je prázdna.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Neplatné kontaktné údaje';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Kontakt bol importovaný.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed =>
|
||||
'Kontakt sa nepodarilo importovať.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Inzerát Zero Hop';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Inzerát povodní';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Kopírovať reklamu do schránky';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard => 'Pridať kontakt z schránky';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kopírovať kontakt do schránky';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Zdieľať kontakt cez inzerát';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Poslal kontakt cez inzerát.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Zlyhalo odoslanie kontaktu.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied =>
|
||||
'Inzerát bol skopírovaný do schránky.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopírovanie inzerátu do schránky zlyhalo.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'Aktivita MeshCore';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'správ',
|
||||
few: 'správy',
|
||||
one: 'správa',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'správ kanálu',
|
||||
few: 'správy kanálu',
|
||||
one: 'správa kanálu',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nových uzlov',
|
||||
few: 'nové uzly',
|
||||
one: 'nový uzol',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Nový $contactType objavený';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Prijatá nová správa';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exportovať repeater / server miestnosti do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exportuje repeater / roomserver s lokalitou do súboru GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Export sprievodcov do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exportuje sprievodcov s umiestnením do súboru GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Exportovať všetky kontakty do GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exportuje všetky kontakty s lokalitou do súboru GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Úspešne exportovaný súbor GPX.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Žiadne kontakty na export.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Nie je podporované na vašom zariadení/operáciomnom systéme';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError => 'Vyskytol sa chyba počas exportu.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Umiestnenia opakovačov a serverov miestností';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Lokácie sprievodcov';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Všetky kontaktné lokality';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Mapové údaje exportované z meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'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?';
|
||||
}
|
||||
|
||||
+802
-165
File diff suppressed because it is too large
Load Diff
@@ -38,6 +38,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get common_delete => 'Radera';
|
||||
|
||||
@override
|
||||
String get common_deleteAll => 'Ta bort alla';
|
||||
|
||||
@override
|
||||
String get common_close => 'Stänga';
|
||||
|
||||
@@ -108,6 +111,89 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_title => 'MeshCore Open';
|
||||
|
||||
@override
|
||||
String get connectionChoiceUsbLabel => 'USB';
|
||||
|
||||
@override
|
||||
String get connectionChoiceBluetoothLabel => 'Bluetooth';
|
||||
|
||||
@override
|
||||
String get usbScreenTitle => 'Anslut via USB';
|
||||
|
||||
@override
|
||||
String get usbScreenSubtitle =>
|
||||
'Välj en detekterad seriell enhet och anslut direkt till din MeshCore-nod.';
|
||||
|
||||
@override
|
||||
String get usbScreenStatus => 'Välj en USB-enhet';
|
||||
|
||||
@override
|
||||
String get usbScreenNote =>
|
||||
'USB-seriell kommunikation är aktiv på stödda Android-enheter och på skrivbordsplattformar.';
|
||||
|
||||
@override
|
||||
String get usbScreenEmptyState =>
|
||||
'Inga USB-enheter hittades. Anslut en och uppdatera.';
|
||||
|
||||
@override
|
||||
String get usbErrorPermissionDenied => 'Tillgången via USB nekas.';
|
||||
|
||||
@override
|
||||
String get usbErrorDeviceMissing =>
|
||||
'Den valda USB-enheten är inte längre tillgänglig.';
|
||||
|
||||
@override
|
||||
String get usbErrorInvalidPort => 'Välj en giltig USB-enhet.';
|
||||
|
||||
@override
|
||||
String get usbErrorBusy =>
|
||||
'En annan förfrågan om USB-anslutning är redan pågående.';
|
||||
|
||||
@override
|
||||
String get usbErrorNotConnected => 'Ingen USB-enhet är ansluten.';
|
||||
|
||||
@override
|
||||
String get usbErrorOpenFailed =>
|
||||
'Misslyckades med att öppna det valda USB-enheten.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectFailed =>
|
||||
'Kunde inte ansluta till det valda USB-enheten.';
|
||||
|
||||
@override
|
||||
String get usbErrorUnsupported =>
|
||||
'USB-seriell kommunikation stöds inte på denna plattform.';
|
||||
|
||||
@override
|
||||
String get usbErrorAlreadyActive => 'En USB-anslutning är redan aktiv.';
|
||||
|
||||
@override
|
||||
String get usbErrorNoDeviceSelected => 'Ingen USB-enhet valdes.';
|
||||
|
||||
@override
|
||||
String get usbErrorPortClosed => 'USB-anslutningen är inte aktiv.';
|
||||
|
||||
@override
|
||||
String get usbErrorConnectTimedOut =>
|
||||
'Anslutningen har tidsutgått. Se till att enheten har rätt USB-firmware.';
|
||||
|
||||
@override
|
||||
String get usbFallbackDeviceName => 'Web-serieenhet';
|
||||
|
||||
@override
|
||||
String get usbStatus_notConnected => 'Välj en USB-enhet';
|
||||
|
||||
@override
|
||||
String get usbStatus_connecting => 'Anslutning till USB-enhet...';
|
||||
|
||||
@override
|
||||
String get usbStatus_searching => 'Söker efter USB-enheter...';
|
||||
|
||||
@override
|
||||
String usbConnectionFailed(String error) {
|
||||
return 'Fel vid USB-anslutning: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get scanner_scanning => 'Söker efter enheter...';
|
||||
|
||||
@@ -142,6 +228,23 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get scanner_scan => 'Skanna';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOff => 'Bluetooth är avstängt';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothOffMessage =>
|
||||
'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
|
||||
String get scanner_enableBluetooth => 'Aktivera Bluetooth';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snabb växling';
|
||||
|
||||
@@ -222,6 +325,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get settings_privacyMode => 'Privatläge';
|
||||
|
||||
@@ -307,6 +417,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_aboutDescription =>
|
||||
'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
|
||||
String get settings_infoName => 'Namn';
|
||||
|
||||
@@ -331,15 +445,6 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_presets => 'Fördefinierade inställningar';
|
||||
|
||||
@override
|
||||
String get settings_preset915Mhz => '915 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset868Mhz => '868 MHz';
|
||||
|
||||
@override
|
||||
String get settings_preset433Mhz => '433 MHz';
|
||||
|
||||
@override
|
||||
String get settings_frequency => 'Frekvens (MHz)';
|
||||
|
||||
@@ -368,10 +473,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_txPowerInvalid => 'Ogiltig TX-effekt (0-22 dBm)';
|
||||
|
||||
@override
|
||||
String get settings_longRange => 'Lång räckvidd';
|
||||
String get settings_clientRepeat => 'Upprepa utan elnät';
|
||||
|
||||
@override
|
||||
String get settings_fastSpeed => 'Snabb hastighet';
|
||||
String get settings_clientRepeatSubtitle =>
|
||||
'Låt enheten repetera nätpaket för andra användare.';
|
||||
|
||||
@override
|
||||
String get settings_clientRepeatFreqWarning =>
|
||||
'För att kunna kommunicera utanför elnätet krävs frekvenserna 433, 869 eller 918 MHz.';
|
||||
|
||||
@override
|
||||
String settings_error(String message) {
|
||||
@@ -441,6 +551,19 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageBg => 'Български';
|
||||
|
||||
@override
|
||||
String get appSettings_languageRu => 'Ryska';
|
||||
|
||||
@override
|
||||
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
|
||||
String get appSettings_notifications => 'Meddelanden';
|
||||
|
||||
@@ -598,6 +721,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get appSettings_noAreaSelected => 'Ingen area markerad';
|
||||
|
||||
@@ -635,7 +767,35 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
'Kontakter kommer att visas när enheter annonserar.';
|
||||
|
||||
@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
|
||||
String get contacts_noUnreadContacts => 'Inga oinlästa kontakter';
|
||||
@@ -761,6 +921,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get channels_deleteChannel => 'Ta bort kanal';
|
||||
|
||||
@@ -769,6 +935,11 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Radera \"$name\"? Detta kan inte ångras.';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleteFailed(String name) {
|
||||
return 'Det gick inte att ta bort kanalen \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String channels_channelDeleted(String name) {
|
||||
return 'Kanalen \"$name\" raderad';
|
||||
@@ -1057,6 +1228,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_pathManagement => 'Stigarhantering';
|
||||
|
||||
@override
|
||||
String get chat_ShowAllPaths => 'Visa alla vägar';
|
||||
|
||||
@override
|
||||
String get chat_routingMode => 'Ruttläge';
|
||||
|
||||
@@ -1192,9 +1366,33 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Olästa: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_openLink => 'Öppna länk?';
|
||||
|
||||
@override
|
||||
String get chat_openLinkConfirmation =>
|
||||
'Vill du öppna den här länken i din webbläsare?';
|
||||
|
||||
@override
|
||||
String get chat_open => 'Öppna';
|
||||
|
||||
@override
|
||||
String chat_couldNotOpenLink(String url) {
|
||||
return 'Kunde inte öppna länken: $url';
|
||||
}
|
||||
|
||||
@override
|
||||
String get chat_invalidLink => 'Ogiltigt länkformat';
|
||||
|
||||
@override
|
||||
String get map_title => 'Nodkarta';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Synlinje';
|
||||
|
||||
@override
|
||||
String get map_losScreenTitle => 'Synlinje';
|
||||
|
||||
@override
|
||||
String get map_noNodesWithLocation => 'Inga noder med platsinformation';
|
||||
|
||||
@@ -1312,6 +1510,13 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_showSharedMarkers => 'Visa delade markörer';
|
||||
|
||||
@override
|
||||
String get map_showGuessedLocations =>
|
||||
'Visa upp de antagna nodernas placeringar';
|
||||
|
||||
@override
|
||||
String get map_guessedLocation => 'Gissad plats';
|
||||
|
||||
@override
|
||||
String get map_lastSeenTime => 'Senaste Visats Tid';
|
||||
|
||||
@@ -1324,6 +1529,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get map_manageRepeater => 'Hantera Upprepare';
|
||||
|
||||
@override
|
||||
String get map_tapToAdd => 'Tryck på noder för att lägga till dem i banan.';
|
||||
|
||||
@override
|
||||
String get map_runTrace => 'Kör spårsökning';
|
||||
|
||||
@override
|
||||
String get map_removeLast => 'Ta bort sista';
|
||||
|
||||
@override
|
||||
String get map_pathTraceCancelled => 'Sökvägsspårning avbruten.';
|
||||
|
||||
@override
|
||||
String get mapCache_title => 'Offline Kartcache';
|
||||
|
||||
@@ -1616,10 +1833,10 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get repeater_cliSubtitle => 'Skicka kommandon till repetitorn';
|
||||
|
||||
@override
|
||||
String get repeater_neighbours => 'Grannar';
|
||||
String get repeater_neighbors => 'Grannar';
|
||||
|
||||
@override
|
||||
String get repeater_neighboursSubtitle => 'Visa noll hoppgrannar.';
|
||||
String get repeater_neighborsSubtitle => 'Visa noll hoppgrannar.';
|
||||
|
||||
@override
|
||||
String get repeater_settings => 'Inställningar';
|
||||
@@ -2310,7 +2527,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get neighbors_repeatersNeighbours => 'Upprepar grannar';
|
||||
String get neighbors_repeatersNeighbors => 'Upprepar grannar';
|
||||
|
||||
@override
|
||||
String get neighbors_noData => 'Inga grannuppgifter finns tillgängliga.';
|
||||
@@ -2541,32 +2758,32 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerateSecret => 'Regenerate Secret';
|
||||
String get community_regenerateSecret => 'Regenerera hemlig kod';
|
||||
|
||||
@override
|
||||
String community_regenerateSecretConfirm(String name) {
|
||||
return 'Regenerate the secret key for \"$name\"? All members will need to scan the new QR code to continue communicating.';
|
||||
return 'Regenerera den hemliga nyckeln för \"$name\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_regenerate => 'Regenerate';
|
||||
String get community_regenerate => 'Regenerera';
|
||||
|
||||
@override
|
||||
String community_secretRegenerated(String name) {
|
||||
return 'Secret regenerated for \"$name\"';
|
||||
return 'Lösenord återskapad för \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String get community_updateSecret => 'Update Secret';
|
||||
String get community_updateSecret => 'Uppdatera hemlighet';
|
||||
|
||||
@override
|
||||
String community_secretUpdated(String name) {
|
||||
return 'Secret updated for \"$name\"';
|
||||
return 'Hemlighet uppdaterad för \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
String community_scanToUpdateSecret(String name) {
|
||||
return 'Scan the new QR code to update the secret for \"$name\"';
|
||||
return 'Skanna den nya QR-koden för att uppdatera hemligheten för \"$name\"';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -2618,6 +2835,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
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
|
||||
String get listFilter_users => 'Användare';
|
||||
|
||||
@@ -2632,4 +2858,412 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get listFilter_newGroup => 'Ny grupp';
|
||||
|
||||
@override
|
||||
String get pathTrace_you => 'Du';
|
||||
|
||||
@override
|
||||
String get pathTrace_failed => 'Sökvägsföljning misslyckades.';
|
||||
|
||||
@override
|
||||
String get pathTrace_notAvailable => 'Path trace ej tillgänglig.';
|
||||
|
||||
@override
|
||||
String get pathTrace_refreshTooltip => 'Uppdatera Path Trace';
|
||||
|
||||
@override
|
||||
String get pathTrace_someHopsNoLocation =>
|
||||
'En eller flera av humlen saknar en plats!';
|
||||
|
||||
@override
|
||||
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
|
||||
String get contacts_pathTrace => 'Path Trace';
|
||||
|
||||
@override
|
||||
String get contacts_ping => 'Ping';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPathTrace => 'Vägspårning till repeater';
|
||||
|
||||
@override
|
||||
String get contacts_repeaterPing => 'Ping-repeater';
|
||||
|
||||
@override
|
||||
String get contacts_roomPathTrace => 'Vägspårning till rumserver';
|
||||
|
||||
@override
|
||||
String get contacts_roomPing => 'Ping rumsserver';
|
||||
|
||||
@override
|
||||
String get contacts_chatTraceRoute => 'Spåra rutt';
|
||||
|
||||
@override
|
||||
String contacts_pathTraceTo(String name) {
|
||||
return 'Spåra rutt till $name';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contacts_clipboardEmpty => 'Urklipp är tomt.';
|
||||
|
||||
@override
|
||||
String get contacts_invalidAdvertFormat => 'Ogiltiga kontaktuppgifter';
|
||||
|
||||
@override
|
||||
String get contacts_contactImported => 'Kontakt har importerats.';
|
||||
|
||||
@override
|
||||
String get contacts_contactImportFailed => 'Kontakt kunde inte importeras.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopAdvert => 'Reklam med nollhopp';
|
||||
|
||||
@override
|
||||
String get contacts_floodAdvert => 'Översvämningsannons';
|
||||
|
||||
@override
|
||||
String get contacts_copyAdvertToClipboard => 'Kopiera annons till urklipp';
|
||||
|
||||
@override
|
||||
String get contacts_addContactFromClipboard =>
|
||||
'Lägg till kontakt från urklipp';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContact => 'Kopiera kontakt till Urklipp';
|
||||
|
||||
@override
|
||||
String get contacts_ShareContactZeroHop => 'Dela kontakt via annons';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertSent => 'Skickat kontakt via annons.';
|
||||
|
||||
@override
|
||||
String get contacts_zeroHopContactAdvertFailed =>
|
||||
'Misslyckades med att skicka kontakt.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopied => 'Annons kopierad till Urklipp.';
|
||||
|
||||
@override
|
||||
String get contacts_contactAdvertCopyFailed =>
|
||||
'Kopiering av annons till Urklipp misslyckades.';
|
||||
|
||||
@override
|
||||
String get notification_activityTitle => 'MeshCore Aktivitet';
|
||||
|
||||
@override
|
||||
String notification_messagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'meddelanden',
|
||||
one: 'meddelande',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_channelMessagesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'kanalmeddelanden',
|
||||
one: 'kanalmeddelande',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newNodesCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'nya noder',
|
||||
one: 'ny nod',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String notification_newTypeDiscovered(String contactType) {
|
||||
return 'Ny $contactType upptäckt';
|
||||
}
|
||||
|
||||
@override
|
||||
String get notification_receivedNewMessage => 'Nytt meddelande mottaget';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeaters =>
|
||||
'Exportera repeater / rumsservrar till GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersSubtitle =>
|
||||
'Exporterar repeater / roomserver med plats till GPX-fil.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContacts => 'Exportera följeslagare till GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportContactsSubtitle =>
|
||||
'Exporterar följeslagare med en plats till GPX-fil.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAll => 'Exportera alla kontakter till GPX';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllSubtitle =>
|
||||
'Exporterar alla kontakter med en plats till GPX-fil.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportSuccess => 'Har exporterat GPX-fil med framgång';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNoContacts => 'Inga kontakter att exportera.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportNotAvailable =>
|
||||
'Stöds inte på din enhet/operativsystem';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportError =>
|
||||
'Det uppstod ett fel när data exporterades.';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportRepeatersRoom =>
|
||||
'Repeater- och rumsserverplatser';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportChat => 'Medhjälparplatser';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportAllContacts => 'Alla kontakters platser';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareText =>
|
||||
'Kartdata exporterad från meshcore-open';
|
||||
|
||||
@override
|
||||
String get settings_gpxExportShareSubject =>
|
||||
'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?';
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+1008
-469
File diff suppressed because it is too large
Load Diff
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kan kanaal {name} niet verwijderen",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "nl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contacten",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Aantal Contacten",
|
||||
"settings_infoChannelCount": "Aantal Kanalen",
|
||||
"settings_presets": "Presets",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequentie (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Ongeldige frequentie (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Vermogen (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ongeldige TX-vermogen (0-22 dBm)",
|
||||
"settings_longRange": "Lange Afstand",
|
||||
"settings_fastSpeed": "Hoge Snelheid",
|
||||
"settings_error": "Fout: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Open kanaal",
|
||||
"channels_privateChannel": "Private kanaal",
|
||||
"channels_editChannel": "Kanaal bewerken",
|
||||
"channels_muteChannel": "Kanaal dempen",
|
||||
"channels_unmuteChannel": "Kanaal dempen opheffen",
|
||||
"channels_deleteChannel": "Kanaal verwijderen",
|
||||
"channels_deleteChannelConfirm": "Verwijderen \"{name}\"? Dit kan niet worden teruggedraaid.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Link openen?",
|
||||
"chat_openLinkConfirmation": "Wilt u deze link in uw browser openen?",
|
||||
"chat_open": "Openen",
|
||||
"chat_couldNotOpenLink": "Kan link niet openen: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Ongeldig linkformaat",
|
||||
"map_title": "Node Map",
|
||||
"map_noNodesWithLocation": "Geen nodes met locatiegegevens",
|
||||
"map_nodesNeedGps": "Nodes moeten hun GPS-coördinaten delen\nom op de kaart te verschijnen",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Buren",
|
||||
"repeater_neighboursSubtitle": "Bekijk nul hops buren.",
|
||||
"repeater_neighbors": "Buren",
|
||||
"repeater_neighborsSubtitle": "Bekijk nul hops buren.",
|
||||
"neighbors_receivedData": "Ontvangen Buurdata",
|
||||
"neighbors_requestTimedOut": "Buren vragen om tijdelijk uitgeschakeld.",
|
||||
"neighbors_errorLoading": "Fout bij het laden van buren: {error}",
|
||||
"neighbors_repeatersNeighbours": "Herhalingen Buren",
|
||||
"neighbors_repeatersNeighbors": "Herhalingen Buren",
|
||||
"neighbors_noData": "Geen gegevens van buren beschikbaar.",
|
||||
"channels_createPrivateChannelDesc": "Beveiligd met een geheime sleutel.",
|
||||
"channels_createPrivateChannel": "Maak een Privé Kanaal",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Dit verwijdert ook {count} kanaal/kanalen en hun berichten.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Community \"{name}\" verlaten",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Open hashtag (iedereen kan deelnemen)",
|
||||
"community_communityHashtag": "Gemeenschappelijk Hashtag",
|
||||
"community_communityHashtagDesc": "Alleen zichtbaar voor leden van de community",
|
||||
"community_forCommunity": "Voor {name}"
|
||||
"community_forCommunity": "Voor {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_secretRegenerated": "Geheim hersteld voor \"{name}\"",
|
||||
"community_regenerateSecret": "Regeneer Geheimwoord",
|
||||
"community_regenerateSecretConfirm": "Regeneere de geheime sleutel voor \"{name}\"? Alle leden moeten de nieuwe QR-code scannen om verder te communiceren.",
|
||||
"community_regenerate": "Regeneer",
|
||||
"community_updateSecret": "Bijwerken Geheime",
|
||||
"community_secretUpdated": "Geheim gewijzigd voor \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scan de nieuwe QR-code om het geheim voor \"{name}\" bij te werken",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Jij",
|
||||
"pathTrace_failed": "Padtrace mislukt.",
|
||||
"pathTrace_notAvailable": "Padtrace niet beschikbaar.",
|
||||
"pathTrace_refreshTooltip": "Path Trace vernieuwen.",
|
||||
"contacts_pathTrace": "Pad Traceren",
|
||||
"contacts_ping": "Pingen",
|
||||
"contacts_repeaterPathTrace": "Pad traceren naar repeater",
|
||||
"contacts_repeaterPing": "Ping repeater",
|
||||
"contacts_roomPathTrace": "Padtrace naar room server",
|
||||
"contacts_roomPing": "Ping kamer server",
|
||||
"contacts_chatTraceRoute": "Route traceren",
|
||||
"contacts_pathTraceTo": "Trace route to {name}",
|
||||
"appSettings_languageUk": "Oekraïens",
|
||||
"contacts_invalidAdvertFormat": "Ongeldige contactgegevens",
|
||||
"contacts_contactImportFailed": "Contact kon niet geïmporteerd worden.",
|
||||
"contacts_zeroHopAdvert": "Zero Hop Reclame",
|
||||
"contacts_floodAdvert": "Overstromingsadvertentie",
|
||||
"contacts_copyAdvertToClipboard": "Advert naar klembord kopiëren",
|
||||
"appSettings_languageRu": "Russisch",
|
||||
"appSettings_enableMessageTracing": "Berichttracking inschakelen",
|
||||
"appSettings_enableMessageTracingSubtitle": "Gedetailleerde routerings- en timing-metadata voor berichten weergeven",
|
||||
"contacts_clipboardEmpty": "Knipbord is leeg.",
|
||||
"contacts_addContactFromClipboard": "Contact uit klembord toevoegen",
|
||||
"contacts_contactImported": "Contact is geïmporteerd.",
|
||||
"contacts_zeroHopContactAdvertSent": "Contact verzonden via advertentie",
|
||||
"contacts_contactAdvertCopied": "Reclame gekopieerd naar Klembord.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiëren van advertentie naar Clipboard is mislukt.",
|
||||
"contacts_ShareContact": "Kontakt naar Klembord kopiëren",
|
||||
"contacts_ShareContactZeroHop": "Contact delen via advertentie",
|
||||
"contacts_zeroHopContactAdvertFailed": "Mislukt om contact te verzenden",
|
||||
"notification_activityTitle": "MeshCore Activiteit",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{bericht} other{berichten}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{kanaalbericht} other{kanaalberichten}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nieuw knooppunt} other{nieuwe knooppunten}}",
|
||||
"notification_newTypeDiscovered": "Nieuw {contactType} ontdekt",
|
||||
"notification_receivedNewMessage": "Nieuw bericht ontvangen",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporteert repeaters / roomserver met een locatie naar GPX-bestand.",
|
||||
"settings_gpxExportRepeaters": "Exporteer repeaters / roomserver naar GPX",
|
||||
"settings_gpxExportSuccess": "Succesvol GPX-bestand geëxporteerd.",
|
||||
"settings_gpxExportNoContacts": "Geen contacten om te exporteren.",
|
||||
"settings_gpxExportNotAvailable": "Niet ondersteund op uw apparaat/besturingssysteem",
|
||||
"settings_gpxExportError": "Er was een fout bij het exporteren.",
|
||||
"settings_gpxExportContacts": "Companions exporteren naar GPX",
|
||||
"settings_gpxExportAll": "Alle contacten exporteren naar GPX",
|
||||
"settings_gpxExportAllSubtitle": "Exporteert alle contacten met een locatie naar een GPX-bestand.",
|
||||
"settings_gpxExportContactsSubtitle": "Exporteert metgezellen met een locatie naar een GPX-bestand.",
|
||||
"settings_gpxExportRepeatersRoom": "Repeater- en kamer servers locaties",
|
||||
"settings_gpxExportChat": "Locaties van metgezellen",
|
||||
"settings_gpxExportAllContacts": "Alle contactlocaties",
|
||||
"settings_gpxExportShareText": "Kaartgegevens geëxporteerd uit meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open GPX kaartgegevens exporteren",
|
||||
"pathTrace_someHopsNoLocation": "Een of meer van de hops ontbreken een locatie!",
|
||||
"map_removeLast": "Verwijder Laatste",
|
||||
"pathTrace_clearTooltip": "Weg wissen",
|
||||
"map_pathTraceCancelled": "Pad traceren geannuleerd",
|
||||
"map_tapToAdd": "Tik op knooppunten om ze toe te voegen aan het pad",
|
||||
"map_runTrace": "Padeshulp traceren",
|
||||
"scanner_enableBluetooth": "Activeer Bluetooth",
|
||||
"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",
|
||||
"snrIndicator_lastSeen": "Laatst gezien",
|
||||
"snrIndicator_nearByRepeaters": "Nabije herhalingseenheden",
|
||||
"chat_ShowAllPaths": "Toon alle paden",
|
||||
"settings_clientRepeat": "Herhalen: Afgekoppeld",
|
||||
"settings_clientRepeatSubtitle": "Laat dit apparaat de mesh-pakketten opnieuw verzenden voor andere apparaten.",
|
||||
"settings_clientRepeatFreqWarning": "Om een signaal buiten het netwerk te versturen, zijn frequenties van 433, 869 of 918 MHz vereist.",
|
||||
"settings_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."
|
||||
}
|
||||
|
||||
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Nie udało się usunąć kanału \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "pl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakty",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Liczba kontaktów",
|
||||
"settings_infoChannelCount": "Liczba kanałów",
|
||||
"settings_presets": "Preset",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Częstotliwość (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Nieprawidłowa częstotliwość (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Moc (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Nieprawidłowa moc TX (0-22 dBm)",
|
||||
"settings_longRange": "Długi zasięg",
|
||||
"settings_fastSpeed": "Szybka prędkość",
|
||||
"settings_error": "Błąd: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Kanał publiczny",
|
||||
"channels_privateChannel": "Prywatny kanał",
|
||||
"channels_editChannel": "Edytuj kanał",
|
||||
"channels_muteChannel": "Wycisz kanał",
|
||||
"channels_unmuteChannel": "Wyłącz wyciszenie kanału",
|
||||
"channels_deleteChannel": "Usuń kanał",
|
||||
"channels_deleteChannelConfirm": "Usuń \"{name}\"? Nie można tego cofnąć.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Otworzyć link?",
|
||||
"chat_openLinkConfirmation": "Czy chcesz otworzyć ten link w przeglądarce?",
|
||||
"chat_open": "Otwórz",
|
||||
"chat_couldNotOpenLink": "Nie można otworzyć linku: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Nieprawidłowy format linku",
|
||||
"map_title": "Mapa węzłów",
|
||||
"map_noNodesWithLocation": "Brak węzłów z danymi lokalizacyjnymi",
|
||||
"map_nodesNeedGps": "Węzły muszą udostępniać swoje współrzędne GPS,\naby pojawić się na mapie.",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Sąsiedzi",
|
||||
"repeater_neighboursSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"repeater_neighbors": "Sąsiedzi",
|
||||
"repeater_neighborsSubtitle": "Wyświetl sąsiedztwo zerowych hopów.",
|
||||
"neighbors_receivedData": "Otrzymano dane sąsiedztwa",
|
||||
"neighbors_requestTimedOut": "Sąsiedzi proszą o wyłączenie timingu.",
|
||||
"neighbors_errorLoading": "Błąd podczas ładowania sąsiadów: {error}",
|
||||
"neighbors_repeatersNeighbours": "Powtarzacze Sąsiedzi",
|
||||
"neighbors_repeatersNeighbors": "Powtarzacze Sąsiedzi",
|
||||
"neighbors_noData": "Brak danych dotyczących sąsiadów.",
|
||||
"channels_joinPrivateChannelDesc": "Ręcznie wprowadź klucz tajny.",
|
||||
"channels_createPrivateChannel": "Utwórz Prywatny Kanał",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Spowoduje to również usunięcie {count} kanału/kanałów i ich wiadomości.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Opuszczono społeczność \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Publiczny hashtag (każdy może dołączyć)",
|
||||
"community_communityHashtag": "Hashtag Społeczności",
|
||||
"community_communityHashtagDesc": "Dostępne tylko dla członków społeczności",
|
||||
"community_forCommunity": "Dla {name}"
|
||||
"community_forCommunity": "Dla {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerate": "Zregeneruj",
|
||||
"community_secretRegenerated": "Hasło ponownie wygenerowane dla \"{name}\"",
|
||||
"community_regenerateSecret": "Zregeneruj sekret",
|
||||
"community_regenerateSecretConfirm": "Regeneruj tajny klucz dla \"{name}\"? Wszyscy członkowie będą musieli zeskanować nowy kod QR, aby kontynuować komunikację.",
|
||||
"community_scanToUpdateSecret": "Skanuj nowy kod QR, aby zaktualizować sekret dla \"{name}\"",
|
||||
"community_secretUpdated": "Hasło zaktualizowane dla \"{name}\"",
|
||||
"community_updateSecret": "Zaktualizuj tajny klucz",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Ty",
|
||||
"pathTrace_failed": "Śledzenie ścieżki nie powiodło się.",
|
||||
"pathTrace_notAvailable": "Ścieżka śledzenia niedostępna.",
|
||||
"contacts_pathTrace": "Śledzenie Ścieżek",
|
||||
"contacts_ping": "Pingować",
|
||||
"contacts_repeaterPathTrace": "Śledzenie ścieżki do repeatera",
|
||||
"contacts_roomPathTrace": "Śledzenie ścieżki do serwera pokojowego",
|
||||
"contacts_roomPing": "Pinguj serwer pokoju",
|
||||
"pathTrace_refreshTooltip": "Odśwież ścieżkę.",
|
||||
"contacts_repeaterPing": "Repeater pingowy",
|
||||
"contacts_pathTraceTo": "Śledź trasę do {name}",
|
||||
"contacts_chatTraceRoute": "Śledź trasę promienia",
|
||||
"appSettings_languageRu": "Rosyjski",
|
||||
"appSettings_languageUk": "Ukraińska",
|
||||
"appSettings_enableMessageTracing": "Włącz śledzenie wiadomości",
|
||||
"appSettings_enableMessageTracingSubtitle": "Pokaż szczegółowe metadane trasowania i czasu dla wiadomości",
|
||||
"contacts_contactImportFailed": "Kontakt nie został zaimportowany.",
|
||||
"contacts_zeroHopAdvert": "Reklama Zero Hop",
|
||||
"contacts_floodAdvert": "Reklama powodziowa",
|
||||
"contacts_copyAdvertToClipboard": "Kopiuj ogłoszenie do schowka",
|
||||
"contacts_clipboardEmpty": "Schowek jest pusty.",
|
||||
"contacts_invalidAdvertFormat": "Nieprawidłowe dane kontaktowe",
|
||||
"contacts_addContactFromClipboard": "Dodaj kontakt z schowka",
|
||||
"contacts_contactImported": "Kontakt został zaimportowany.",
|
||||
"contacts_zeroHopContactAdvertSent": "Wysłano kontakt przez ogłoszenie.",
|
||||
"contacts_contactAdvertCopied": "Reklama skopiowana do schowka.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiowanie ogłoszenia do schowka nie powiodło się.",
|
||||
"contacts_ShareContactZeroHop": "Udostępnij kontakt przez ogłoszenie",
|
||||
"contacts_ShareContact": "Kopiuj kontakt do schowka",
|
||||
"contacts_zeroHopContactAdvertFailed": "Nie udało się wysłać kontaktu.",
|
||||
"notification_activityTitle": "Aktywność MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{wiadomość} few{wiadomości} many{wiadomości} other{wiadomości}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{wiadomość kanału} few{wiadomości kanału} many{wiadomości kanału} other{wiadomości kanału}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nowy węzeł} few{nowe węzły} many{nowych węzłów} other{nowych węzłów}}",
|
||||
"notification_newTypeDiscovered": "Nowy {contactType} wykryty",
|
||||
"notification_receivedNewMessage": "Otrzymano nową wiadomość",
|
||||
"settings_gpxExportContacts": "Eksportuj towarzyszy do GPX",
|
||||
"settings_gpxExportRepeaters": "Eksportuj powtórki / serwer pokojowy do GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Eksportuje powtarzacze / roomserver z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportSuccess": "Pomyślnie wyeksportowano plik GPX.",
|
||||
"settings_gpxExportNotAvailable": "Nie obsługiwane na Twoim urządzeniu/systemie operacyjnym",
|
||||
"settings_gpxExportError": "Wystąpił błąd podczas eksportowania.",
|
||||
"settings_gpxExportRepeatersRoom": "Lokalizacje serwerów powtarzających i pomieszczeń",
|
||||
"settings_gpxExportContactsSubtitle": "Eksportuje towarzyszy z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportAll": "Eksportuj wszystkie kontakty do GPX",
|
||||
"settings_gpxExportAllSubtitle": "Eksportuje wszystkie kontakty z lokalizacją do pliku GPX.",
|
||||
"settings_gpxExportAllContacts": "Wszystkie lokalizacje kontaktów",
|
||||
"settings_gpxExportNoContacts": "Brak kontaktów do wyeksportowania.",
|
||||
"settings_gpxExportChat": "Lokalizacje towarzyszy",
|
||||
"settings_gpxExportShareText": "Dane mapy wyeksportowane z meshcore-open",
|
||||
"settings_gpxExportShareSubject": "Eksport danych mapy GPX meshcore-open",
|
||||
"pathTrace_someHopsNoLocation": "Jeden lub więcej z chmieli nie ma określonej lokalizacji!",
|
||||
"map_pathTraceCancelled": "Śledzenie ścieżki anulowano.",
|
||||
"map_runTrace": "Uruchom ślad ścieżki",
|
||||
"pathTrace_clearTooltip": "Wyczyść ścieżkę",
|
||||
"map_removeLast": "Usuń ostatni",
|
||||
"map_tapToAdd": "Kliknij na węzły, aby dodać je do ścieżki.",
|
||||
"scanner_bluetoothOffMessage": "Prosimy włączyć Bluetooth, aby przeskanować urządzenia.",
|
||||
"scanner_chromeRequired": "Wymagana przeglądarka Chrome",
|
||||
"scanner_chromeRequiredMessage": "Ta aplikacja internetowa wymaga przeglądarki Google Chrome lub opartej na Chromium do obsługi Bluetooth.",
|
||||
"scanner_bluetoothOff": "Bluetooth jest wyłączony",
|
||||
"scanner_enableBluetooth": "Włącz Bluetooth",
|
||||
"snrIndicator_lastSeen": "Ostatnio widziany",
|
||||
"snrIndicator_nearByRepeaters": "Nadajniki w pobliżu",
|
||||
"chat_ShowAllPaths": "Pokaż wszystkie ścieżki",
|
||||
"settings_clientRepeatSubtitle": "Pozwól temu urządzeniu powtarzać pakiety danych dla innych urządzeń.",
|
||||
"settings_clientRepeat": "Powtórzenie: Niezależne od sieci",
|
||||
"settings_clientRepeatFreqWarning": "Powtórka poza siecią wymaga częstotliwości 433, 869 lub 918 MHz.",
|
||||
"settings_aboutOpenMeteoAttribution": "Dane wysokościowe LOS: Open-Meteo (CC BY 4.0)",
|
||||
"appSettings_unitsTitle": "Jednostki",
|
||||
"appSettings_unitsMetric": "Metryczne (m / km)",
|
||||
"appSettings_unitsImperial": "Imperialne (ft / mi)",
|
||||
"map_lineOfSight": "Linia wzroku",
|
||||
"map_losScreenTitle": "Linia wzroku",
|
||||
"losSelectStartEnd": "Wybierz węzły początkowe i końcowe dla LOS.",
|
||||
"losRunFailed": "Sprawdzenie pola widzenia nie powiodło się: {error}",
|
||||
"@losRunFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losClearAllPoints": "Wyczyść wszystkie punkty",
|
||||
"losRunToViewElevationProfile": "Uruchom LOS, aby wyświetlić profil wysokości",
|
||||
"losMenuTitle": "Menu LOS",
|
||||
"losMenuSubtitle": "Stuknij węzły lub naciśnij i przytrzymaj mapę, aby uzyskać niestandardowe punkty",
|
||||
"losShowDisplayNodes": "Pokaż węzły wyświetlające",
|
||||
"losCustomPoints": "Punkty niestandardowe",
|
||||
"losCustomPointLabel": "Niestandardowe {index}",
|
||||
"@losCustomPointLabel": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losPointA": "Punkt A",
|
||||
"losPointB": "Punkt B",
|
||||
"losAntennaA": "Antena A: {value} {unit}",
|
||||
"@losAntennaA": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losAntennaB": "Antena B: {value} {unit}",
|
||||
"@losAntennaB": {
|
||||
"placeholders": {
|
||||
"value": {
|
||||
"type": "String"
|
||||
},
|
||||
"unit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losRun": "Uruchom LOS-a",
|
||||
"losNoElevationData": "Brak danych o wysokości",
|
||||
"losProfileClear": "{distance} {distanceUnit}, czysty LOS, minimalny prześwit {clearance} {heightUnit}",
|
||||
"@losProfileClear": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"clearance": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losProfileBlocked": "{distance} {distanceUnit}, zablokowane przez {obstruction} {heightUnit}",
|
||||
"@losProfileBlocked": {
|
||||
"placeholders": {
|
||||
"distance": {
|
||||
"type": "String"
|
||||
},
|
||||
"distanceUnit": {
|
||||
"type": "String"
|
||||
},
|
||||
"obstruction": {
|
||||
"type": "String"
|
||||
},
|
||||
"heightUnit": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losStatusChecking": "LOS: sprawdzam...",
|
||||
"losStatusNoData": "LOS: brak danych",
|
||||
"losStatusSummary": "LOS: {clear}/{total} jasne, {blocked} zablokowane, {unknown} nieznane",
|
||||
"@losStatusSummary": {
|
||||
"placeholders": {
|
||||
"clear": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
},
|
||||
"blocked": {
|
||||
"type": "int"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"losErrorElevationUnavailable": "Dane dotyczące wysokości są niedostępne dla jednej lub większej liczby próbek.",
|
||||
"losErrorInvalidInput": "Nieprawidłowe dane punktów/wysokości do obliczenia LOS.",
|
||||
"losRenameCustomPoint": "Zmień nazwę punktu niestandardowego",
|
||||
"losPointName": "Nazwa punktu",
|
||||
"losShowPanelTooltip": "Pokaż panel LOS",
|
||||
"losHidePanelTooltip": "Ukryj panel LOS",
|
||||
"losElevationAttribution": "Dane dotyczące wysokości: Open-Meteo (CC BY 4.0)",
|
||||
"losLegendRadioHorizon": "Horyzont radiowy",
|
||||
"losLegendLosBeam": "Linia widoczności",
|
||||
"losLegendTerrain": "Teren",
|
||||
"losFrequencyLabel": "Częstotliwość",
|
||||
"losFrequencyInfoTooltip": "Zobacz szczegóły obliczenia",
|
||||
"losFrequencyDialogTitle": "Obliczanie horyzontu radiowego",
|
||||
"losFrequencyDialogDescription": "Zaczynając od k={baselineK} przy {baselineFreq} MHz, obliczenia korygują współczynnik k dla bieżącego pasma {frequencyMHz} MHz, które definiuje zakrzywiony limit horyzontu radiowego.",
|
||||
"@losFrequencyDialogDescription": {
|
||||
"description": "Explain how the calculation uses the baseline frequency and derived k-factor.",
|
||||
"placeholders": {
|
||||
"baselineK": {
|
||||
"type": "double"
|
||||
},
|
||||
"baselineFreq": {
|
||||
"type": "double"
|
||||
},
|
||||
"frequencyMHz": {
|
||||
"type": "double"
|
||||
},
|
||||
"kFactor": {
|
||||
"type": "double"
|
||||
}
|
||||
}
|
||||
},
|
||||
"listFilter_removeFromFavorites": "Usuń z ulubionych",
|
||||
"listFilter_addToFavorites": "Dodaj do ulubionych",
|
||||
"listFilter_favorites": "Ulubione",
|
||||
"@contacts_searchFavorites": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchUsers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRepeaters": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@contacts_searchRoomServers": {
|
||||
"placeholders": {
|
||||
"number": {
|
||||
"type": "int"
|
||||
},
|
||||
"str": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_unread": "Nieprzeczytane",
|
||||
"contacts_searchContactsNoNumber": "Wyszukaj kontakty...",
|
||||
"contacts_searchFavorites": "Wyszukaj {number}{str} ulubione...",
|
||||
"contacts_searchRoomServers": "Wyszukaj {number}{str} serwerów Room...",
|
||||
"contacts_searchUsers": "Wyszukaj {number}{str} Użytkowników...",
|
||||
"contacts_searchRepeaters": "Wyszukaj {number}{str} powtórników...",
|
||||
"contactsSettings_title": "Ustawienia kontaktów",
|
||||
"settings_contactSettingsSubtitle": "Ustawienia dotyczące sposobu dodawania kontaktów",
|
||||
"contactsSettings_autoAddUsersSubtitle": "Pozwól towarzyszowi automatycznie dodawać znalezione użytkowników.",
|
||||
"contactsSettings_autoAddRepeatersTitle": "Automatyczne dodawanie powtarzalników",
|
||||
"contactsSettings_autoAddRepeatersSubtitle": "Zezwól na automatyczne dodawanie odkrytych repeaterów przez towarzysza.",
|
||||
"contactsSettings_autoAddRoomServersTitle": "Automatycznie dodaj serwery pokojowe",
|
||||
"contactsSettings_autoAddUsersTitle": "Automatycznie dodaj użytkowników",
|
||||
"settings_contactSettings": "Ustawienia kontaktowe",
|
||||
"contactsSettings_otherTitle": "Inne ustawienia związane z kontaktami",
|
||||
"contactsSettings_autoAddTitle": "Automatyczne odnajdywanie",
|
||||
"contactsSettings_autoAddRoomServersSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie znalezionych serwerów pokojowych.",
|
||||
"contactsSettings_autoAddSensorsTitle": "Automatycznie dodaj czujniki",
|
||||
"discoveredContacts_searchHint": "Wyszukaj odkryte kontakty",
|
||||
"discoveredContacts_contactAdded": "Kontakt dodany",
|
||||
"discoveredContacts_addContact": "Dodaj kontakt",
|
||||
"discoveredContacts_copyContact": "Kopiuj kontakt do schowka",
|
||||
"contactsSettings_overwriteOldestTitle": "Nadpisz najstarszy",
|
||||
"discoveredContacts_Title": "Odkryte Kontakty",
|
||||
"contactsSettings_autoAddSensorsSubtitle": "Zezwól towarzyszowi na automatyczne dodawanie wykrytych czujników.",
|
||||
"discoveredContacts_noMatching": "Brak pasujących kontaktów",
|
||||
"discoveredContacts_deleteContact": "Usuń kontakt",
|
||||
"contactsSettings_overwriteOldestSubtitle": "Gdy lista kontaktów jest pełna, najstarszy nieulubiony kontakt zostanie zastąpiony.",
|
||||
"common_deleteAll": "Usuń wszystko",
|
||||
"discoveredContacts_deleteContactAllContent": "Czy na pewno chcesz usunąć wszystkie znalezione kontakty?",
|
||||
"discoveredContacts_deleteContactAll": "Usuń wszystkie odkryte kontakty",
|
||||
"map_guessedLocation": "Wydana lokalizacja",
|
||||
"map_showGuessedLocations": "Wyświetl lokalizacje zgadanych węzłów",
|
||||
"usbScreenSubtitle": "Wybierz wykryty urządzenie szeregowe i podłącz je bezpośrednio do swojego węzła MeshCore.",
|
||||
"usbScreenTitle": "Połącz przez USB",
|
||||
"connectionChoiceUsbLabel": "USB",
|
||||
"connectionChoiceBluetoothLabel": "Bluetooth",
|
||||
"usbScreenStatus": "Wybierz urządzenie USB",
|
||||
"usbScreenNote": "Port szeregowy USB jest aktywny na urządzeniach z systemem Android i platformach stacjonarnych, które go obsługują.",
|
||||
"usbScreenEmptyState": "Nie znaleziono żadnych urządzeń USB. Podłącz jedno i zaktualizuj.",
|
||||
"usbErrorPermissionDenied": "Zostało odrzucone żądanie dostępu przez USB.",
|
||||
"usbErrorDeviceMissing": "Wybór urządzenia USB już nie jest dostępny.",
|
||||
"usbErrorInvalidPort": "Wybierz prawidłowe urządzenie USB.",
|
||||
"usbErrorBusy": "Kolejne żądanie połączenia przez USB jest już w trakcie realizacji.",
|
||||
"usbErrorNotConnected": "Brak podłączonego urządzenia USB.",
|
||||
"usbErrorOpenFailed": "Nie udało się otworzyć wybranego urządzenia USB.",
|
||||
"usbErrorConnectFailed": "Nie udało się połączyć z wybranym urządzeniem USB.",
|
||||
"usbErrorUnsupported": "Port szeregowy USB nie jest obsługiwany na tym urządzeniu.",
|
||||
"usbErrorAlreadyActive": "Połączenie USB jest już aktywne.",
|
||||
"usbErrorNoDeviceSelected": "Nie został wybrany żaden urządzenie USB.",
|
||||
"usbErrorPortClosed": "Połączenie USB nie jest aktywne.",
|
||||
"usbFallbackDeviceName": "Urządzenie do komunikacji przez sieć (seria)",
|
||||
"@usbConnectionFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"usbStatus_searching": "Wyszukiwanie urządzeń USB...",
|
||||
"usbStatus_connecting": "Połączenie z urządzeniem USB...",
|
||||
"usbStatus_notConnected": "Wybierz urządzenie USB",
|
||||
"usbConnectionFailed": "Błąd połączenia USB: {error}",
|
||||
"usbErrorConnectTimedOut": "Połączenie nie zostało nawiązane. Upewnij się, że urządzenie posiada oprogramowanie \"USB Companion\"."
|
||||
}
|
||||
|
||||
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Falha ao excluir o canal \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "pt",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Contactos",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Número de Contatos",
|
||||
"settings_infoChannelCount": "Número do Canal",
|
||||
"settings_presets": "Presets",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frequência (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Frequência inválida (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Potência (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Potência de TX inválida (0-22 dBm)",
|
||||
"settings_longRange": "Alcance Longo",
|
||||
"settings_fastSpeed": "Velocidade Rápida",
|
||||
"settings_error": "Erro: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Canal público",
|
||||
"channels_privateChannel": "Canal privado",
|
||||
"channels_editChannel": "Editar canal",
|
||||
"channels_muteChannel": "Silenciar canal",
|
||||
"channels_unmuteChannel": "Ativar canal",
|
||||
"channels_deleteChannel": "Excluir canal",
|
||||
"channels_deleteChannelConfirm": "Excluir \"{name}\"? Não pode ser desfeito.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Abrir link?",
|
||||
"chat_openLinkConfirmation": "Deseja abrir este link no seu navegador?",
|
||||
"chat_open": "Abrir",
|
||||
"chat_couldNotOpenLink": "Não foi possível abrir o link: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Formato de link inválido",
|
||||
"map_title": "Mapa de Nós",
|
||||
"map_noNodesWithLocation": "Não existem nós com dados de localização.",
|
||||
"map_nodesNeedGps": "Os nós precisam partilhar as suas coordenadas GPS\npara aparecerem no mapa",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Vizinhos",
|
||||
"repeater_neighbors": "Vizinhos",
|
||||
"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_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.",
|
||||
"channels_createPrivateChannelDesc": "Protegido com uma chave secreta.",
|
||||
"channels_joinPrivateChannelDesc": "Inserir uma chave secreta manualmente.",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Isso também excluirá {count} canal/canais e suas mensagens.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Saiu da comunidade \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Hashtag público (qualquer pessoa pode participar)",
|
||||
"community_communityHashtag": "Hashtag da Comunidade",
|
||||
"community_communityHashtagDesc": "Apenas para membros da comunidade",
|
||||
"community_forCommunity": "Para {name}"
|
||||
"community_forCommunity": "Para {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerateSecretConfirm": "Regenerar a chave secreta para \"{name}\"? Todos os membros precisarão escanear o novo código QR para continuar a comunicação.",
|
||||
"community_regenerateSecret": "Regenerar Senha Segura",
|
||||
"community_secretRegenerated": "Senha secreta regenerada para \"{name}\"",
|
||||
"community_regenerate": "Regenerar",
|
||||
"community_secretUpdated": "Segredo atualizado para \"{name}\"",
|
||||
"community_scanToUpdateSecret": "Scanar o novo código QR para atualizar o segredo para \"{name}\"\n\n\n+++++",
|
||||
"community_updateSecret": "Atualizar Segredo",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Você",
|
||||
"pathTrace_failed": "Falha no rastreamento de caminho.",
|
||||
"pathTrace_notAvailable": "Traçado de caminho não disponível.",
|
||||
"pathTrace_refreshTooltip": "Atualizar Path Trace.",
|
||||
"contacts_pathTrace": "Traçado de Caminho",
|
||||
"contacts_ping": "Pingar",
|
||||
"contacts_repeaterPathTrace": "Traçar caminho para repetidor",
|
||||
"contacts_repeaterPing": "Pingar repetidor",
|
||||
"contacts_roomPathTrace": "Traçar caminho para o servidor da sala",
|
||||
"contacts_roomPing": "Pingar servidor da sala",
|
||||
"contacts_chatTraceRoute": "Rastrear rota do caminho",
|
||||
"contacts_pathTraceTo": "Rastrear rota para {name}",
|
||||
"contacts_invalidAdvertFormat": "Dados de Contato Inválidos",
|
||||
"contacts_clipboardEmpty": "Área de Transferência Está Vazia.",
|
||||
"appSettings_languageUk": "Ucraniano",
|
||||
"contacts_contactImported": "Contato foi importado.",
|
||||
"contacts_zeroHopAdvert": "Anúncio Zero Hop",
|
||||
"contacts_copyAdvertToClipboard": "Copiar Anúncio para Área de Transferência",
|
||||
"contacts_addContactFromClipboard": "Adicionar Contato da Área de Transferência",
|
||||
"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_contactImportFailed": "Contato falhou ao ser importado.",
|
||||
"contacts_zeroHopContactAdvertSent": "Enviou contato por anúncio.",
|
||||
"contacts_contactAdvertCopied": "Anúncio copiado para a Área de Transferência.",
|
||||
"contacts_floodAdvert": "Anúncio de Inundação",
|
||||
"contacts_contactAdvertCopyFailed": "Cópia do anúncio para a Área de Transferência falhou.",
|
||||
"contacts_ShareContactZeroHop": "Compartilhar contato por anúncio",
|
||||
"contacts_zeroHopContactAdvertFailed": "Falha ao enviar contato.",
|
||||
"notification_activityTitle": "Atividade MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{mensagem} other{mensagens}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{mensagem de canal} other{mensagens de canal}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{novo nó} other{novos nós}}",
|
||||
"notification_newTypeDiscovered": "Novo {contactType} descoberto",
|
||||
"notification_receivedNewMessage": "Nova mensagem recebida",
|
||||
"settings_gpxExportRepeaters": "Exportar repetidores / servidor de sala para GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporta repetidores / roomserver com localização para arquivo GPX.",
|
||||
"settings_gpxExportSuccess": "Arquivo GPX exportado com sucesso.",
|
||||
"settings_gpxExportAllSubtitle": "Exporta todos os contatos com uma localização para um arquivo GPX.",
|
||||
"settings_gpxExportNotAvailable": "Não suportado no seu dispositivo/SO",
|
||||
"settings_gpxExportError": "Ocorreu um erro ao exportar.",
|
||||
"settings_gpxExportAll": "Exportar todos os contatos para GPX",
|
||||
"settings_gpxExportContacts": "Exportar companheiros para GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Exporta companheiros com uma localização para um arquivo GPX.",
|
||||
"settings_gpxExportRepeatersRoom": "Localizações do servidor de repetidor e sala",
|
||||
"settings_gpxExportChat": "Localizações de companheiros",
|
||||
"settings_gpxExportNoContacts": "Nenhum contato para exportar.",
|
||||
"settings_gpxExportAllContacts": "Todos os locais de contatos",
|
||||
"settings_gpxExportShareText": "Dados do mapa exportados do meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open exportação de dados de mapa GPX",
|
||||
"pathTrace_someHopsNoLocation": "Um ou mais dos lúpulos estão sem localização!",
|
||||
"map_runTrace": "Executar Traçado de Caminho",
|
||||
"map_pathTraceCancelled": "Rastreamento de caminho cancelado.",
|
||||
"pathTrace_clearTooltip": "Limpar caminho",
|
||||
"map_removeLast": "Remover Último",
|
||||
"map_tapToAdd": "Toque nos nós para adicioná-los ao caminho.",
|
||||
"scanner_enableBluetooth": "Ative o Bluetooth",
|
||||
"scanner_bluetoothOff": "Bluetooth está desativado",
|
||||
"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_clientRepeat": "Repetição sem rede",
|
||||
"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."
|
||||
}
|
||||
|
||||
+1103
File diff suppressed because it is too large
Load Diff
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanál \"{name}\" sa nepodarilo odstrániť",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sk",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakty",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Počet kontaktov",
|
||||
"settings_infoChannelCount": "Počet kanálov",
|
||||
"settings_presets": "Prednastavenia",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvencia (MHz)",
|
||||
"settings_frequencyHelper": "300,0 – 2500,0",
|
||||
"settings_frequencyInvalid": "Neplatná frekvencia (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX Výkon (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Neplatná hodnota výkonu TX (0-22 dBm)",
|
||||
"settings_longRange": "Dlhý dosah",
|
||||
"settings_fastSpeed": "Rýchla rýchlosť",
|
||||
"settings_error": "Chyba: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Veľké verejne kanály",
|
||||
"channels_privateChannel": "Osobné 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_deleteChannelConfirm": "Odstrániť \"{name}\"? To sa nedá zrušiť.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Otvoriť odkaz?",
|
||||
"chat_openLinkConfirmation": "Chcete otvoriť tento odkaz v prehliadači?",
|
||||
"chat_open": "Otvoriť",
|
||||
"chat_couldNotOpenLink": "Nepodarilo sa otvoriť odkaz: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Neplatný formát odkazu",
|
||||
"map_title": "Mapa uzlov",
|
||||
"map_noNodesWithLocation": "Žiadne uzly s údajmi o polohe",
|
||||
"map_nodesNeedGps": "Uholníky musia zdieľať svoje GPS súradnice, aby sa zobrazili na mape.",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Zobraziť susedné body bez skokov.",
|
||||
"repeater_neighborsSubtitle": "Zobraziť susedné body bez skokov.",
|
||||
"neighbors_requestTimedOut": "Súďia žiadajú o časové ukončenie.",
|
||||
"neighbors_receivedData": "Obdielo dáta suseda",
|
||||
"repeater_neighbours": "Súsezný",
|
||||
"repeater_neighbors": "Súsezný",
|
||||
"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.",
|
||||
"channels_createPrivateChannel": "Vytvorte súkromný kanál",
|
||||
"channels_joinPrivateChannel": "Pripojiť sa k súkromnému kanálu",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Tým sa tiež vymaže {count} kanál/kanálov a ich správy.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Opustená komunita \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Veľký hashtag (ktočokoľvek sa môže pridať)",
|
||||
"community_communityHashtag": "Komunitný Hashtag",
|
||||
"community_communityHashtagDesc": "Špecifické pre členov komunity",
|
||||
"community_forCommunity": "Pre {name}"
|
||||
"community_forCommunity": "Pre {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_secretRegenerated": "Záznam pre \"{name}\" bol regenerovaný tajne",
|
||||
"community_regenerateSecretConfirm": "Znovu vygenerovať tajný kľúč pre \"{name}\"? Všetci členovia budú musieť skanovať nový QR kód, aby mohli nadviazať komunikáciu.",
|
||||
"community_regenerate": "Znovu vygenerovať",
|
||||
"community_regenerateSecret": "Zobraziť nový tajný kód",
|
||||
"community_scanToUpdateSecret": "Skáňte nový QR kód na aktualizáciu tajného hesla pre \"{name}\"",
|
||||
"community_updateSecret": "Aktualizovať tajné heslo",
|
||||
"community_secretUpdated": "Zmena tajnej slova pre \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Vy",
|
||||
"pathTrace_failed": "Sledovanie cesty zlyhalo.",
|
||||
"pathTrace_notAvailable": "Path trace nie je k dispozícii.",
|
||||
"pathTrace_refreshTooltip": "Obnoviť Path Trace.",
|
||||
"contacts_pathTrace": "Sledovanie lúčov",
|
||||
"contacts_ping": "Pingovať",
|
||||
"contacts_repeaterPathTrace": "Sledovanie cesty k opakovaču",
|
||||
"contacts_repeaterPing": "Pingovať opakovač",
|
||||
"contacts_roomPathTrace": "Sledovanie cesty k serveru miestnosti",
|
||||
"contacts_roomPing": "Ping server miestnosti",
|
||||
"contacts_chatTraceRoute": "Sledovať trasu lúča",
|
||||
"contacts_pathTraceTo": "Sledovať trasu k {name}",
|
||||
"contacts_clipboardEmpty": "Schránka je prázdna.",
|
||||
"appSettings_languageUk": "Ukrajinská",
|
||||
"contacts_contactImportFailed": "Kontakt sa nepodarilo importovať.",
|
||||
"contacts_zeroHopAdvert": "Inzerát Zero Hop",
|
||||
"contacts_floodAdvert": "Inzerát povodní",
|
||||
"contacts_copyAdvertToClipboard": "Kopírovať reklamu do schránky",
|
||||
"contacts_invalidAdvertFormat": "Neplatné kontaktné údaje",
|
||||
"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_contactImported": "Kontakt bol importovaný.",
|
||||
"contacts_zeroHopContactAdvertSent": "Poslal kontakt cez inzerát.",
|
||||
"contacts_contactAdvertCopied": "Inzerát bol skopírovaný do schránky.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopírovanie inzerátu do schránky zlyhalo.",
|
||||
"contacts_zeroHopContactAdvertFailed": "Zlyhalo odoslanie kontaktu.",
|
||||
"contacts_ShareContactZeroHop": "Zdieľať kontakt cez inzerát",
|
||||
"contacts_ShareContact": "Kopírovať kontakt do schránky",
|
||||
"notification_activityTitle": "Aktivita MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{správa} few{správy} other{správ}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{správa kanálu} few{správy kanálu} other{správ kanálu}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{nový uzol} few{nové uzly} other{nových uzlov}}",
|
||||
"notification_newTypeDiscovered": "Nový {contactType} objavený",
|
||||
"notification_receivedNewMessage": "Prijatá nová správa",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exportuje repeater / roomserver s lokalitou do súboru GPX.",
|
||||
"settings_gpxExportContacts": "Export sprievodcov do GPX",
|
||||
"settings_gpxExportSuccess": "Úspešne exportovaný súbor GPX.",
|
||||
"settings_gpxExportNoContacts": "Žiadne kontakty na export.",
|
||||
"settings_gpxExportNotAvailable": "Nie je podporované na vašom zariadení/operáciomnom systéme",
|
||||
"settings_gpxExportRepeatersRoom": "Umiestnenia opakovačov a serverov miestností",
|
||||
"settings_gpxExportError": "Vyskytol sa chyba počas exportu.",
|
||||
"settings_gpxExportAllSubtitle": "Exportuje všetky kontakty s lokalitou do súboru GPX.",
|
||||
"settings_gpxExportContactsSubtitle": "Exportuje sprievodcov s umiestnením do súboru GPX.",
|
||||
"settings_gpxExportRepeaters": "Exportovať repeater / server miestnosti do GPX",
|
||||
"settings_gpxExportAll": "Exportovať všetky kontakty do GPX",
|
||||
"settings_gpxExportAllContacts": "Všetky kontaktné lokality",
|
||||
"settings_gpxExportChat": "Lokácie sprievodcov",
|
||||
"settings_gpxExportShareText": "Mapové údaje exportované z meshcore-open",
|
||||
"settings_gpxExportShareSubject": "meshcore-open export dát GPX mapových údajov",
|
||||
"pathTrace_someHopsNoLocation": "Jedna alebo viac chmeľov chýba lokalita!",
|
||||
"pathTrace_clearTooltip": "Zmazať cestu",
|
||||
"map_tapToAdd": "Kliknite na uzly, aby ste ich pridali k ceste.",
|
||||
"map_removeLast": "Odstrániť posledný",
|
||||
"map_runTrace": "Spustiť trasovaním cesty",
|
||||
"map_pathTraceCancelled": "Zrušenie stopáže cesty bolo zrušené.",
|
||||
"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_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_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_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."
|
||||
}
|
||||
|
||||
+521
-146
@@ -1,7 +1,15 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Kanala {name} ni bilo mogoče izbrisati",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sl",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakti",
|
||||
"nav_contacts": "Stiki",
|
||||
"nav_channels": "Kanali",
|
||||
"nav_map": "Karta",
|
||||
"common_cancel": "Prekliči",
|
||||
@@ -69,49 +77,49 @@
|
||||
},
|
||||
"scanner_stop": "Prekliči",
|
||||
"scanner_scan": "Skeniraj",
|
||||
"device_quickSwitch": "Hitro preklopiti",
|
||||
"device_quickSwitch": "Hitro preklop",
|
||||
"device_meshcore": "MeshCore",
|
||||
"settings_title": "Nastavitve",
|
||||
"settings_deviceInfo": "Informacije o napravei",
|
||||
"settings_appSettings": "Nastavitve aplikacije",
|
||||
"settings_appSettingsSubtitle": "Obveščanja, sporoščanje in zemljevidi.",
|
||||
"settings_nodeSettings": "Nastavitve časa",
|
||||
"settings_nodeName": "Ime omrežno mesto",
|
||||
"settings_nodeNameNotSet": "Nezavedeno",
|
||||
"settings_nodeNameHint": "Vnesite ime časa",
|
||||
"settings_nodeSettings": "Nastavitev časa",
|
||||
"settings_nodeName": "Ime node-a",
|
||||
"settings_nodeNameNotSet": "Ni nastavljeno",
|
||||
"settings_nodeNameHint": "Vnesite ime node-a",
|
||||
"settings_nodeNameUpdated": "Ime posodobljeno",
|
||||
"settings_radioSettings": "Nastavitve radija",
|
||||
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširni faktor",
|
||||
"settings_radioSettingsSubtitle": "Frekvenca, moč, razširitveni faktor",
|
||||
"settings_radioSettingsUpdated": "Radio nastavitve posodobljene",
|
||||
"settings_location": "Lokacija",
|
||||
"settings_locationSubtitle": "GPS koordinate",
|
||||
"settings_locationUpdated": "Lokacija posodobljena",
|
||||
"settings_locationBothRequired": "Vnesite širino in dolžino.",
|
||||
"settings_locationInvalid": "Neveljna zemeljska širina ali dolžina.",
|
||||
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
|
||||
"settings_latitude": "Širina",
|
||||
"settings_longitude": "Dolžina",
|
||||
"settings_privacyMode": "Mod podjetja",
|
||||
"settings_privacyMode": "Zasebnost",
|
||||
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
|
||||
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo v oglasih.",
|
||||
"settings_privacyModeEnabled": "Privatni režim je omogočen.",
|
||||
"settings_privacyModeDisabled": "Privatni režim je onemogočen.",
|
||||
"settings_privacyModeEnabled": "Privatni način je omogočen.",
|
||||
"settings_privacyModeDisabled": "Privatni način je onemogočen.",
|
||||
"settings_actions": "Akcije",
|
||||
"settings_sendAdvertisement": "Pošlji Oglas",
|
||||
"settings_sendAdvertisementSubtitle": "Trenutna prisotnost v oddajah",
|
||||
"settings_advertisementSent": "Oglas poslan",
|
||||
"settings_syncTime": "Ugasniti čas",
|
||||
"settings_syncTimeSubtitle": "Nastavi uro naprave v čas telefona",
|
||||
"settings_timeSynchronized": "Sinhronizirano po času",
|
||||
"settings_syncTime": "Nastavi uro",
|
||||
"settings_syncTimeSubtitle": "Nastavi uro naprave na čas telefona",
|
||||
"settings_timeSynchronized": "Ura sinhronizirana",
|
||||
"settings_refreshContacts": "Ponovno obišči kontakte",
|
||||
"settings_refreshContactsSubtitle": "Ponovno naloži seznam kontaktov iz naprave",
|
||||
"settings_rebootDevice": "Restart Naprave",
|
||||
"settings_rebootDeviceSubtitle": "Ponovite zažetek naprave MeshCore",
|
||||
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagon napravke? Boste odvisni od omrežja.",
|
||||
"settings_debug": "Napravi popravek",
|
||||
"settings_bleDebugLog": "Logarjev zapis BLE",
|
||||
"settings_bleDebugLogSubtitle": "Navodila BLE, odgovori in surovo podatkovno",
|
||||
"settings_appDebugLog": "Log zapiske aplikacije",
|
||||
"settings_appDebugLogSubtitle": "Prijavni sporočila aplikacije",
|
||||
"settings_refreshContactsSubtitle": "Ponovno naloži seznam stikov v napravi",
|
||||
"settings_rebootDevice": "Ponovni zagon naprave",
|
||||
"settings_rebootDeviceSubtitle": "Ponovno zaženi MeshCore napravo",
|
||||
"settings_rebootDeviceConfirm": "Ste prepričani, da želite ponovno zagnati napravo? Povezava bo prekinjena.",
|
||||
"settings_debug": "Debug",
|
||||
"settings_bleDebugLog": "BLE debug log (razhroščevanje)",
|
||||
"settings_bleDebugLogSubtitle": "BLE ukazi, odgovori in surovi podatki",
|
||||
"settings_appDebugLog": "Logi aplikacije",
|
||||
"settings_appDebugLogSubtitle": "Debug sporočila aplikacije",
|
||||
"settings_about": "Oglejte si",
|
||||
"settings_aboutVersion": "MeshCore Open v{version}",
|
||||
"@settings_aboutVersion": {
|
||||
@@ -121,30 +129,25 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_aboutLegalese": "MeshCore Odprtokodni Projekt 2024",
|
||||
"settings_aboutDescription": "Odprtokodni Flutter kličnik za naprave za LoRa mrežo MeshCore.",
|
||||
"settings_aboutLegalese": "Odprtokodni projekt MeshCore 2024",
|
||||
"settings_aboutDescription": "Odprtokodni Flutter klient za naprave za LoRa omrežje MeshCore.",
|
||||
"settings_infoName": "Ime",
|
||||
"settings_infoId": "ID",
|
||||
"settings_infoStatus": "Status",
|
||||
"settings_infoBattery": "Baterija",
|
||||
"settings_infoPublicKey": "Ključ javnega tipa",
|
||||
"settings_infoContactsCount": "Število kontaktov",
|
||||
"settings_infoPublicKey": "Javni ključ",
|
||||
"settings_infoContactsCount": "Število stikov",
|
||||
"settings_infoChannelCount": "Število kanalov",
|
||||
"settings_presets": "Prednastavitve",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvenca (MHz)",
|
||||
"settings_frequencyHelper": "300,00 - 2500,00",
|
||||
"settings_frequencyInvalid": "Neveljčna frekvenca (300-2500 MHz)",
|
||||
"settings_frequencyInvalid": "Neveljavna frekvenca (300-2500 MHz)",
|
||||
"settings_bandwidth": "Pasovna širina",
|
||||
"settings_spreadingFactor": "Razširitveni faktor",
|
||||
"settings_codingRate": "Programska hitrost",
|
||||
"settings_txPower": "TX Moč (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Neveljaven TX moč (0-22 dBm)",
|
||||
"settings_longRange": "Dolenje območje",
|
||||
"settings_fastSpeed": "Hitra hitrost",
|
||||
"settings_txPowerInvalid": "Neveljavna TX moč (0-22 dBm)",
|
||||
"settings_error": "Napaka: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -156,8 +159,8 @@
|
||||
"appSettings_title": "Nastavitve aplikacije",
|
||||
"appSettings_appearance": "Prikaži",
|
||||
"appSettings_theme": "Tema",
|
||||
"appSettings_themeSystem": "Predpomnilnik sistema",
|
||||
"appSettings_themeLight": "Luč",
|
||||
"appSettings_themeSystem": "Sistemska tema",
|
||||
"appSettings_themeLight": "Svetlo",
|
||||
"appSettings_themeDark": "Temno",
|
||||
"appSettings_language": "Jezik",
|
||||
"appSettings_languageSystem": "Sistemska privzeta vrednost",
|
||||
@@ -174,8 +177,8 @@
|
||||
"appSettings_languageNl": "Nederlands",
|
||||
"appSettings_languageSk": "Slovenčina",
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_notifications": "Obveščanja",
|
||||
"appSettings_enableNotifications": "Omogoči obveščanje",
|
||||
"appSettings_notifications": "Obvestila",
|
||||
"appSettings_enableNotifications": "Omogoči obvestila",
|
||||
"appSettings_enableNotificationsSubtitle": "Prejmite obvestila o sporočilih in oglasih",
|
||||
"appSettings_notificationPermissionDenied": "Odobritev obvestila zavrnjena",
|
||||
"appSettings_notificationsEnabled": "Obvestila omogočena",
|
||||
@@ -185,19 +188,19 @@
|
||||
"appSettings_channelMessageNotifications": "Obvestila o sporočilih kanala",
|
||||
"appSettings_channelMessageNotificationsSubtitle": "Pokaži obvestilo ob prejemanju sporočil kanala",
|
||||
"appSettings_advertisementNotifications": "Opozorila o oglasih",
|
||||
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so novi vozlišči odkrivljeni.",
|
||||
"appSettings_advertisementNotificationsSubtitle": "Pokaži obvestilo, ko so najdene nove naprave.",
|
||||
"appSettings_messaging": "Komuniciranje",
|
||||
"appSettings_clearPathOnMaxRetry": "Ponovite pot do cilja na največjem štetju",
|
||||
"appSettings_clearPathOnMaxRetrySubtitle": "Ponovi pot zimske obveščevalne poti po 5 neuspešnih poskusih pošiljanja",
|
||||
"appSettings_pathsWillBeCleared": "Potnice bodo očiščene po 5 neuspešnih poskusih.",
|
||||
"appSettings_pathsWillNotBeCleared": "Potniški poti ne bodo samodejno čiščeni.",
|
||||
"appSettings_autoRouteRotation": "Avtomatsko Občutke in Rotacije",
|
||||
"appSettings_autoRouteRotationSubtitle": "Med spreminjanjem med najboljšimi potmi in plovilnim načinom",
|
||||
"appSettings_pathsWillBeCleared": "Počisti pot po 5 neuspešnih poskusih.",
|
||||
"appSettings_pathsWillNotBeCleared": "Poti ne bodo samodejno čiščene.",
|
||||
"appSettings_autoRouteRotation": "Avtomatsko rotacija prenosne poti",
|
||||
"appSettings_autoRouteRotationSubtitle": "Menjaj med boljšo potjo in flood načinom",
|
||||
"appSettings_autoRouteRotationEnabled": "Samodejno krmilno rotiranje omogočeno",
|
||||
"appSettings_autoRouteRotationDisabled": "Samodejno krmilno rotiranje je onemogočeno",
|
||||
"appSettings_battery": "Baterija",
|
||||
"appSettings_batteryChemistry": "Razem z možnostmi",
|
||||
"appSettings_batteryChemistryPerDevice": "Nastavitve za naprave ({deviceName})",
|
||||
"appSettings_batteryChemistry": "Kemija baterije",
|
||||
"appSettings_batteryChemistryPerDevice": "Nastavitev za napravo ({deviceName})",
|
||||
"@appSettings_batteryChemistryPerDevice": {
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
@@ -205,20 +208,20 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_batteryChemistryConnectFirst": "Povežite se z napravo za izbiro",
|
||||
"appSettings_batteryChemistryConnectFirst": "Za izbiro se poveži z napravo",
|
||||
"appSettings_batteryNmc": "18650 NMC (3,0-4,2V)",
|
||||
"appSettings_batteryLifepo4": "LiFePO4 (2,6–3,65 V)",
|
||||
"appSettings_batteryLipo": "LiPo (3,0-4,2V)",
|
||||
"appSettings_mapDisplay": "Prikaz zemljevide",
|
||||
"appSettings_showRepeaters": "Prikaži ponovitve",
|
||||
"appSettings_showRepeatersSubtitle": "Prikaži ponovljalne notranjosti na zemljeploscu",
|
||||
"appSettings_showChatNodes": "Prikaži čakalne notranjosti",
|
||||
"appSettings_showChatNodesSubtitle": "Prikaži pogovorni pike na zemljeploscu",
|
||||
"appSettings_showOtherNodes": "Pokaži druge vozlišča",
|
||||
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste notranjih elementov na zemljevalu.",
|
||||
"appSettings_timeFilter": "Filtri po času",
|
||||
"appSettings_timeFilterShowAll": "Pokaži vse notranje elemente",
|
||||
"appSettings_timeFilterShowLast": "Pokaži notranjosti iz zadnjih {hours} ur",
|
||||
"appSettings_mapDisplay": "Prikaz zemljevida",
|
||||
"appSettings_showRepeaters": "Prikaži repetitorje",
|
||||
"appSettings_showRepeatersSubtitle": "Prikaži repetitorje na mapi",
|
||||
"appSettings_showChatNodes": "Prikaži naprave za klepet",
|
||||
"appSettings_showChatNodesSubtitle": "Prikaži naprave na zemljevidu",
|
||||
"appSettings_showOtherNodes": "Pokaži druge naprave",
|
||||
"appSettings_showOtherNodesSubtitle": "Pokaži druge vrste naprav na zemljevidu.",
|
||||
"appSettings_timeFilter": "Filter po času",
|
||||
"appSettings_timeFilterShowAll": "Pokaži vse naprave",
|
||||
"appSettings_timeFilterShowLast": "Pokaži naprave v zadnjih {hours} urah",
|
||||
"@appSettings_timeFilterShowLast": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -226,15 +229,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_mapTimeFilter": "Filtri časa zemljevida",
|
||||
"appSettings_showNodesDiscoveredWithin": "Pokaži notranje čepke, odkrivene v:",
|
||||
"appSettings_allTime": "Vse čase",
|
||||
"appSettings_lastHour": "Minuto nazaj",
|
||||
"appSettings_mapTimeFilter": "Filter časa na zemljevidu",
|
||||
"appSettings_showNodesDiscoveredWithin": "Pokaži naprave odkrite v:",
|
||||
"appSettings_allTime": "Brez omejitev",
|
||||
"appSettings_lastHour": "V zadnji uri",
|
||||
"appSettings_last6Hours": "Zadnjih 6 ur",
|
||||
"appSettings_last24Hours": "Zadnjih 24 ur",
|
||||
"appSettings_lastWeek": "Lepošno",
|
||||
"appSettings_offlineMapCache": "Omrezni Poudni Arhiv",
|
||||
"appSettings_noAreaSelected": "Nizkana označena površina",
|
||||
"appSettings_lastWeek": "Prejšnji teden",
|
||||
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
|
||||
"appSettings_noAreaSelected": "Območje ni izbrano",
|
||||
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
|
||||
"@appSettings_areaSelectedZoom": {
|
||||
"placeholders": {
|
||||
@@ -246,19 +249,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_debugCard": "Napravi popravek",
|
||||
"appSettings_appDebugLogging": "Programski Log",
|
||||
"appSettings_appDebugLoggingSubtitle": "Log aplikacijske debug sporočila za odpravljanje težav",
|
||||
"appSettings_appDebugLoggingEnabled": "Omogočeno zaznamovanje napak v aplikaciji",
|
||||
"appSettings_appDebugLoggingDisabled": "Programski logi aplikacije so onemogočeni.",
|
||||
"contacts_title": "Kontakti",
|
||||
"contacts_noContacts": "Še ni kontaktov.",
|
||||
"contacts_contactsWillAppear": "Kontakti se bodo prikazali, ko naprave oglasijo.",
|
||||
"contacts_searchContacts": "Iskanje kontaktov...",
|
||||
"contacts_noUnreadContacts": "Nerešeno kontaktov.",
|
||||
"contacts_noContactsFound": "Niti ena oseba ali skupine ni najdena.",
|
||||
"contacts_deleteContact": "Izbrisati Kontakt",
|
||||
"contacts_removeConfirm": "Izbrisati {contactName} iz kontaktov?",
|
||||
"appSettings_debugCard": "Razhroščevanje",
|
||||
"appSettings_appDebugLogging": "Programski dnevnik",
|
||||
"appSettings_appDebugLoggingSubtitle": "Dnevnik debug sporočil za odpravljanje težav",
|
||||
"appSettings_appDebugLoggingEnabled": "Beleženje napak v aplikaciji omogočeno",
|
||||
"appSettings_appDebugLoggingDisabled": "Beleženje napak v aplikacije onemogočeno.",
|
||||
"contacts_title": "Stiki",
|
||||
"contacts_noContacts": "Ni stikov.",
|
||||
"contacts_contactsWillAppear": "Stiki se bodo prikazali, ko se naprave oglasijo.",
|
||||
"contacts_searchContacts": "Iskanje stikov...",
|
||||
"contacts_noUnreadContacts": "Ne prebrani stiki.",
|
||||
"contacts_noContactsFound": "Stiki niso najdeni.",
|
||||
"contacts_deleteContact": "Izbriši stik",
|
||||
"contacts_removeConfirm": "Izbrišem {contactName} iz stikov?",
|
||||
"@contacts_removeConfirm": {
|
||||
"placeholders": {
|
||||
"contactName": {
|
||||
@@ -266,12 +269,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_manageRepeater": "Upravljajte Ponovitve",
|
||||
"contacts_roomLogin": "Vnos v sobo",
|
||||
"contacts_openChat": "Odprta kleta",
|
||||
"contacts_editGroup": "Uredi Skupino",
|
||||
"contacts_deleteGroup": "Izbrisati Skupino",
|
||||
"contacts_deleteGroupConfirm": "Odpovedati {groupName}?",
|
||||
"contacts_manageRepeater": "Upravljaj Ponovitve",
|
||||
"contacts_roomLogin": "Prijava v sobo",
|
||||
"contacts_openChat": "Odpri klepet",
|
||||
"contacts_editGroup": "Uredi skupino",
|
||||
"contacts_deleteGroup": "Izbriši skupino",
|
||||
"contacts_deleteGroupConfirm": "Izbriši {groupName}?",
|
||||
"@contacts_deleteGroupConfirm": {
|
||||
"placeholders": {
|
||||
"groupName": {
|
||||
@@ -279,8 +282,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_newGroup": "Novo skupino",
|
||||
"contacts_groupName": "Skupina imena",
|
||||
"contacts_newGroup": "Nova skupina",
|
||||
"contacts_groupName": "Ime skupine",
|
||||
"contacts_groupNameRequired": "Ime skupine je obvezno.",
|
||||
"contacts_groupAlreadyExists": "Skupina \"{name}\" že obstaja",
|
||||
"@contacts_groupAlreadyExists": {
|
||||
@@ -290,11 +293,11 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_filterContacts": "Filtri kontakt\\,...",
|
||||
"contacts_noContactsMatchFilter": "Niti ena oseba ne ustreza vašemu kriteriju.",
|
||||
"contacts_noMembers": "Nič članov.",
|
||||
"contacts_lastSeenNow": "Datum zadnjega vpisa zdaj",
|
||||
"contacts_lastSeenMinsAgo": "Zadnjič videti {minutes} minut nazaj",
|
||||
"contacts_filterContacts": "Filtriraj stik\\,...",
|
||||
"contacts_noContactsMatchFilter": "Noben stik ne ustreza vašemu kriteriju.",
|
||||
"contacts_noMembers": "Ni članov.",
|
||||
"contacts_lastSeenNow": "Nazadnje viden zdaj",
|
||||
"contacts_lastSeenMinsAgo": "Zadnjič viden pred {minutes} minutami",
|
||||
"@contacts_lastSeenMinsAgo": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
@@ -302,8 +305,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenHourAgo": "Zadnjič ogledan pred 1 uro.",
|
||||
"contacts_lastSeenHoursAgo": "Zadnjič videti {hours} ur nazaj",
|
||||
"contacts_lastSeenHourAgo": "Zadnjič viden pred 1 uro.",
|
||||
"contacts_lastSeenHoursAgo": "Zadnjič viden pred {hours} urami",
|
||||
"@contacts_lastSeenHoursAgo": {
|
||||
"placeholders": {
|
||||
"hours": {
|
||||
@@ -311,8 +314,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"contacts_lastSeenDayAgo": "Zadnjič ogledan pred 1 dnem",
|
||||
"contacts_lastSeenDaysAgo": "Zadnjič videti {days} dni nazaj",
|
||||
"contacts_lastSeenDayAgo": "Zadnjič viden pred 1 dnem",
|
||||
"contacts_lastSeenDaysAgo": "Zadnjič viden pred {days} dnem",
|
||||
"@contacts_lastSeenDaysAgo": {
|
||||
"placeholders": {
|
||||
"days": {
|
||||
@@ -321,10 +324,10 @@
|
||||
}
|
||||
},
|
||||
"channels_title": "Kanali",
|
||||
"channels_noChannelsConfigured": "Nekonfigurirane kanale",
|
||||
"channels_addPublicChannel": "Dodaj Objavni Kanal",
|
||||
"channels_noChannelsConfigured": "Kanali še niso konfigurirani",
|
||||
"channels_addPublicChannel": "Dodaj javni kanal",
|
||||
"channels_searchChannels": "Poišči kanale...",
|
||||
"channels_noChannelsFound": "Niti kanalov najti ni.",
|
||||
"channels_noChannelsFound": "Ne najdem kanalov.",
|
||||
"channels_channelIndex": "Kanal {index}",
|
||||
"@channels_channelIndex": {
|
||||
"placeholders": {
|
||||
@@ -334,13 +337,15 @@
|
||||
}
|
||||
},
|
||||
"channels_hashtagChannel": "Hashtag kanal",
|
||||
"channels_public": "javno",
|
||||
"channels_private": "Zasebno",
|
||||
"channels_publicChannel": "Ogljišna skupina",
|
||||
"channels_privateChannel": "Zatemniščen kanal",
|
||||
"channels_public": "Javni",
|
||||
"channels_private": "Zasebni",
|
||||
"channels_publicChannel": "Javni kanal",
|
||||
"channels_privateChannel": "Zasebni kanal",
|
||||
"channels_editChannel": "Uredi kanal",
|
||||
"channels_muteChannel": "Utišaj kanal",
|
||||
"channels_unmuteChannel": "Vklopi obvestila kanala",
|
||||
"channels_deleteChannel": "Pošlji kanal",
|
||||
"channels_deleteChannelConfirm": "Izbrisati \"{name}\"? To se ne da povrniti.",
|
||||
"channels_deleteChannelConfirm": "Izbrišem \"{name}\"? To se ne da povrniti.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
@@ -424,8 +429,8 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_typeMessage": "Vnesite sporočilo...",
|
||||
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} bajt).",
|
||||
"chat_typeMessage": "Vnesi sporočilo...",
|
||||
"chat_messageTooLong": "Pošiljanje sporočila je onemogočeno, saj je preveliko (maksimalno {maxBytes} byte-ov).",
|
||||
"@chat_messageTooLong": {
|
||||
"placeholders": {
|
||||
"maxBytes": {
|
||||
@@ -433,9 +438,9 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_messageCopied": "Pošljeno sporočilo",
|
||||
"chat_messageDeleted": "Pošiljanje sporočila izbrisano",
|
||||
"chat_retryingMessage": "Ponovna poskus.",
|
||||
"chat_messageCopied": "Sporočilo poslano",
|
||||
"chat_messageDeleted": "Sporočilo izbrisano",
|
||||
"chat_retryingMessage": "Ponovni poskus.",
|
||||
"chat_retryCount": "Ponovit {current}/{max}",
|
||||
"@chat_retryCount": {
|
||||
"placeholders": {
|
||||
@@ -448,31 +453,31 @@
|
||||
}
|
||||
},
|
||||
"chat_sendGif": "Pošlji GIF",
|
||||
"chat_reply": "Odpošlji",
|
||||
"chat_addReaction": "Dodaj Reakcijo",
|
||||
"chat_reply": "Odgovori",
|
||||
"chat_addReaction": "Dodaj reakcijo",
|
||||
"chat_me": "jaz",
|
||||
"emojiCategorySmileys": "Emoji",
|
||||
"emojiCategoryGestures": "Gestikulacije",
|
||||
"emojiCategoryHearts": "Srce",
|
||||
"emojiCategoryObjects": "Predmeti",
|
||||
"gifPicker_title": "Izberi GIF",
|
||||
"gifPicker_searchHint": "Iskalite GIF-e...",
|
||||
"gifPicker_poweredBy": "Naprodno z GIPHY",
|
||||
"gifPicker_noGifsFound": "Niti GIF-jev najti ni.",
|
||||
"gifPicker_failedLoad": "Neuspešno je naložilo GIF-e",
|
||||
"gifPicker_failedSearch": "Posodobit neuspešno.",
|
||||
"gifPicker_searchHint": "Išči GIF-e...",
|
||||
"gifPicker_poweredBy": "Napredno z GIPHY",
|
||||
"gifPicker_noGifsFound": "Ne najdem GIF-ov.",
|
||||
"gifPicker_failedLoad": "Neuspešno nalaganje GIF-a",
|
||||
"gifPicker_failedSearch": "Iskanje neuspešno.",
|
||||
"gifPicker_noInternet": "Ni internetne povezave",
|
||||
"debugLog_appTitle": "Log zapiske aplikacije",
|
||||
"debugLog_bleTitle": "Logarjev zapis BLE",
|
||||
"debugLog_copyLog": "Kopiraj zapiske",
|
||||
"debugLog_clearLog": "Pasters log",
|
||||
"debugLog_copied": "Kopirana belež poteka.",
|
||||
"debugLog_bleCopied": "Kopirana beležke iz BLE",
|
||||
"debugLog_noEntries": "Še ni ustvarjenih debug zapisov.",
|
||||
"debugLog_enableInSettings": "Omogoči beleženje napak v aplikaciji v nastavitvah",
|
||||
"debugLog_frames": "Okna",
|
||||
"debugLog_bleTitle": "Log zapis BLE",
|
||||
"debugLog_copyLog": "Kopiraj dnevnik",
|
||||
"debugLog_clearLog": "Briši log",
|
||||
"debugLog_copied": "Beležka kopirana.",
|
||||
"debugLog_bleCopied": "Kopirana beležka iz BLE",
|
||||
"debugLog_noEntries": "Ni ustvarjenih debug zapisov.",
|
||||
"debugLog_enableInSettings": "Omogoči beleženje napak v nastavitvah aplikacije",
|
||||
"debugLog_frames": "Okvirji",
|
||||
"debugLog_rawLogRx": "Svež Log-RX",
|
||||
"debugLog_noBleActivity": "Šele začnite z aktivnostjo BLE.",
|
||||
"debugLog_noBleActivity": "Ni BLE aktivnosti.",
|
||||
"debugFrame_length": "Izhodni rob: {count} bajtov",
|
||||
"@debugFrame_length": {
|
||||
"placeholders": {
|
||||
@@ -542,8 +547,8 @@
|
||||
"chat_forceFloodMode": "Nasilje obvezati v način",
|
||||
"chat_recentAckPaths": "Nedavni poti ACK (tap za uporabo):",
|
||||
"chat_pathHistoryFull": "Zapiske o poti so popolni. Izbriši vnose, da dodaš nove.",
|
||||
"chat_hopSingular": "skoč",
|
||||
"chat_hopPlural": "škrabec",
|
||||
"chat_hopSingular": "skok",
|
||||
"chat_hopPlural": "skokov",
|
||||
"chat_hopsCount": "{count} {count, plural, =1{hop} other{hops}}",
|
||||
"@chat_hopsCount": {
|
||||
"placeholders": {
|
||||
@@ -554,16 +559,16 @@
|
||||
},
|
||||
"chat_successes": "Uspešni",
|
||||
"chat_removePath": "Izbriši pot",
|
||||
"chat_noPathHistoryYet": "Še ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
|
||||
"chat_noPathHistoryYet": "Ni shranjenih poti.\nPošlji sporočilo za odkrivanje poti.",
|
||||
"chat_pathActions": "Potni ukazi:",
|
||||
"chat_setCustomPath": "Nastavi Prilozeno Pot",
|
||||
"chat_setCustomPathSubtitle": "Ročno določite potniško pot.",
|
||||
"chat_clearPath": "Čista pot",
|
||||
"chat_clearPath": "Počisti pot",
|
||||
"chat_clearPathSubtitle": "Ob naslednji pošiljanju znova zbrati.",
|
||||
"chat_pathCleared": "Pot je očiščena. Naslednje sporočilo bo ponovno odkril pot.",
|
||||
"chat_floodModeSubtitle": "Uporabi tipko usmerjevanja v meniju aplikacije.",
|
||||
"chat_floodModeEnabled": "Narejena je bila omrežna modaliteta. Vklopi jo znova preko ikone v meniju aplikacije.",
|
||||
"chat_fullPath": "Polni pot",
|
||||
"chat_fullPath": "Polna pot",
|
||||
"chat_pathDetailsNotAvailable": "Podrobnosti poti zaenkrat niso na voljo. Poskusite poslati sporočilo za osvežitev.",
|
||||
"chat_pathSetHops": "Pot nastavljen: {hopCount} {hopCount, plural, =1{hop} other{hops}} - {status}",
|
||||
"@chat_pathSetHops": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Odpreti povezavo?",
|
||||
"chat_openLinkConfirmation": "Ali želite odpreti to povezavo v brskalniku?",
|
||||
"chat_open": "Odpri",
|
||||
"chat_couldNotOpenLink": "Povezave ni bilo mogoče odpreti: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Neveljavna oblika povezave",
|
||||
"map_title": "Mapa omrežja",
|
||||
"map_noNodesWithLocation": "Nihče od notranjih elementov nima podatkov o lokaciji.",
|
||||
"map_nodesNeedGps": "Omrežje morajo deliti svoje GPS koordinate,\nda se prikazao na zemljeobrazniku.",
|
||||
@@ -1092,13 +1109,13 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_cliQuickGetName": "Dobiti ime",
|
||||
"repeater_cliQuickGetName": "Pridobi ime",
|
||||
"repeater_cliQuickGetRadio": "Dobiti Radiopravo",
|
||||
"repeater_cliQuickGetTx": "Dobiti TX",
|
||||
"repeater_cliQuickGetTx": "Pridobi TX",
|
||||
"repeater_cliQuickNeighbors": "Sosedi",
|
||||
"repeater_cliQuickVersion": "Različica",
|
||||
"repeater_cliQuickAdvertise": "Oglasite",
|
||||
"repeater_cliQuickClock": "Urnik",
|
||||
"repeater_cliQuickClock": "Ura",
|
||||
"repeater_cliHelpAdvert": "Pošlje paket oglasov",
|
||||
"repeater_cliHelpReboot": "Ponastavi naprave. (Opomba, lahko pride do 'Timeouta', kar je normalno)",
|
||||
"repeater_cliHelpClock": "Prikaže trenutno uro po uri naprave.",
|
||||
@@ -1130,7 +1147,7 @@
|
||||
"repeater_cliHelpSetBridgeSecret": "Nastavi skrivni dostop za mostove ESPNOW.",
|
||||
"repeater_cliHelpSetAdcMultiplier": "Nastavi prilagoditev faktorja za prilagoditev poravnalnega napetosti baterije (podprt le na izbranih ploščah).",
|
||||
"repeater_cliHelpTempRadio": "Nastavi začasne radio parametre za določeno časovno obdobje, kar po preteku časa vrne originalne radio parametre. (ne shranjuje v preferencije).",
|
||||
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustreznu vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
|
||||
"repeater_cliHelpSetPerm": "Modificira ACL. Odstrani ustrezen vnos (po predponi pubkeyja), če je \"permissions\" enako nič. Dodaja nov vnos, če je pubkey-hex v celoti in trenutno ni v ACL. Posodobi vnos po ustreznem predponi pubkeyja. Bitje dovoljenj se razlikuje glede na firmware vlogo, vendar so prvi dve bitki: 0 (Gost), 1 (Lezenje samo), 2 (Lezenje in pisanje), 3 (Administrator).",
|
||||
"repeater_cliHelpGetBridgeType": "Dobrodošli pri izbiri vrste mostu: brez, rs232, espnow",
|
||||
"repeater_cliHelpLogStart": "Začnete beleženje paketov v datotekovni sistem.",
|
||||
"repeater_cliHelpLogStop": "Ustavite beleženje paketov v datotečno sistem.",
|
||||
@@ -1159,8 +1176,8 @@
|
||||
"repeater_settingsCategory": "Nastavitve",
|
||||
"repeater_bridge": "Most",
|
||||
"repeater_logging": "Logiranje",
|
||||
"repeater_neighborsRepeaterOnly": "Sosedi (le za ponovitelja)",
|
||||
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za ponovitve)",
|
||||
"repeater_neighborsRepeaterOnly": "Sosedi (le za repetitorje)",
|
||||
"repeater_regionManagementRepeaterOnly": "Upravljanje regij (zgolj za repetitorje)",
|
||||
"repeater_regionNote": "Regionske ukazi so bili uvedeni za upravljanje z regijskimi definicijami in dovolili.",
|
||||
"repeater_gpsManagement": "Upravljanje GPS",
|
||||
"repeater_gpsNote": "GPS ukaz je bil uveden za upravljanje z vprašanji, povezanimi z lokacijo.",
|
||||
@@ -1232,9 +1249,9 @@
|
||||
"channelPath_repeaterHops": "Skoki ponovitelja",
|
||||
"channelPath_noHopDetails": "Podrobnosti o paketu za dostavo niso navedene.",
|
||||
"channelPath_messageDetails": "Podrobnosti sporočila",
|
||||
"channelPath_senderLabel": "Pošiljalec",
|
||||
"channelPath_timeLabel": "Čas",
|
||||
"channelPath_repeatsLabel": "Ponovi",
|
||||
"channelPath_senderLabel": "Pošiljatelj",
|
||||
"channelPath_timeLabel": "Ura",
|
||||
"channelPath_repeatsLabel": "Ponovitve",
|
||||
"channelPath_pathLabel": "Pot {index}",
|
||||
"channelPath_observedLabel": "Opazovani",
|
||||
"channelPath_observedPathTitle": "Opazovana pot {index} • {hops}",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighboursSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||
"repeater_neighbours": "Sosedi",
|
||||
"repeater_neighborsSubtitle": "Pogledati nič sosednjih hopjev.",
|
||||
"repeater_neighbors": "Sosedi",
|
||||
"neighbors_receivedData": "Prejeto podatke o sosedih",
|
||||
"neighbors_requestTimedOut": "Sosedi zahtevajo izklop po dogovoru.",
|
||||
"neighbors_errorLoading": "Napaka pri obnašanju sosedov: {error}",
|
||||
"neighbors_repeatersNeighbours": "Ponovitve Sosedi",
|
||||
"neighbors_repeatersNeighbors": "Ponovitve Sosedi",
|
||||
"neighbors_noData": "Niso na voljo podatki o sosedih.",
|
||||
"channels_joinPrivateChannel": "Pridružite se zasebni skupini",
|
||||
"channels_createPrivateChannelDesc": "Varno zaklenjeno s skrivnim ključem.",
|
||||
@@ -1466,23 +1483,381 @@
|
||||
"community_addPublicChannel": "Dodaj Objavni Kanal Komunitarja",
|
||||
"community_addPublicChannelHint": "Samodejno dodaj javni kanal za to skupnost.",
|
||||
"community_noCommunities": "Še nobena skupnost se ni pridružila.",
|
||||
"community_scanOrCreate": "Skenirajte QR kodo ali ustvarite skupnost za začetek.",
|
||||
"community_manageCommunities": "Upravljajte skupnosti",
|
||||
"community_scanOrCreate": "Skeniraj QR kodo ali ustvari skupnost za začetek.",
|
||||
"community_manageCommunities": "Upravljanje skupnosti",
|
||||
"community_delete": "Opusti skupnost",
|
||||
"community_deleteConfirm": "Zapustiti \"{name}\"?",
|
||||
"community_deleteConfirm": "Zapusti \"{name}\"?",
|
||||
"community_deleteChannelsWarning": "To bo izbrisalo tudi {count} kanal/kanalov in njihova sporočila.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Zapustil skupnost \"{name}\"",
|
||||
"community_addHashtagChannel": "Dodaj Oznako Obštnine",
|
||||
"community_addHashtagChannel": "Dodaj hashtag kanal",
|
||||
"community_addHashtagChannelDesc": "Dodajte hashtag kanal za to skupnost.",
|
||||
"community_selectCommunity": "Izberi skupnost",
|
||||
"community_regularHashtag": "Oznaka s hashtagom",
|
||||
"community_regularHashtagDesc": "javna oznaka (kateri koli lahko sodelujejo)",
|
||||
"community_regularHashtagDesc": "javna oznaka (kdorkoli lahko sodeluje)",
|
||||
"community_communityHashtag": "Skupnostni hashtag",
|
||||
"community_communityHashtagDesc": "Izključeno za uporabnike skupnosti",
|
||||
"community_forCommunity": "Za {name}"
|
||||
"community_forCommunity": "Za {name}",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_secretRegenerated": "Geslo za \"{name}\" ponovno ustvarjeno",
|
||||
"community_regenerateSecret": "Ponovno ustvari geslo",
|
||||
"community_regenerateSecretConfirm": "Preberite novo tajno geslo za \"{name}\"? Vsi članici morajo prebrati novo QR kodo, da lahko nadaljujejo s komunikacijo.",
|
||||
"community_regenerate": "Preberi znova",
|
||||
"community_scanToUpdateSecret": "Skeniraj novo QR kodo za posodabljanje ključa za {name}",
|
||||
"community_updateSecret": "Ažuriraj ključ",
|
||||
"community_secretUpdated": "Skrivnostno spremembo za \"{name}\"",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Ti",
|
||||
"pathTrace_failed": "Sledenje poti ni uspelo.",
|
||||
"pathTrace_notAvailable": "Potni sled ni na voljo.",
|
||||
"pathTrace_refreshTooltip": "Osveži Path Trace.",
|
||||
"contacts_pathTrace": "Sledenje poti",
|
||||
"contacts_ping": "Pingati",
|
||||
"contacts_repeaterPathTrace": "Sledi poti do ponavljalnika",
|
||||
"contacts_repeaterPing": "Pinguj ponavljalnik",
|
||||
"contacts_roomPathTrace": "Sledenje poti do strežnika sobe",
|
||||
"contacts_roomPing": "Ping strežnik sobe",
|
||||
"contacts_chatTraceRoute": "Slediti poti žarkov",
|
||||
"contacts_pathTraceTo": "Trace route to {name}",
|
||||
"appSettings_languageRu": "Ruščina",
|
||||
"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_contactImportFailed": "Kontakt ni bil uspešno uvožen.",
|
||||
"contacts_zeroHopAdvert": "Reklama brez posrednikov",
|
||||
"contacts_floodAdvert": "Poplavna oglás",
|
||||
"contacts_invalidAdvertFormat": "Neveljavni kontaktne podatke",
|
||||
"contacts_clipboardEmpty": "Odložišče je prazno.",
|
||||
"contacts_copyAdvertToClipboard": "Kopiraj oglas v odložišče",
|
||||
"contacts_addContactFromClipboard": "Dodaj stik iz odložišča",
|
||||
"contacts_zeroHopContactAdvertSent": "Poslano po oglasu.",
|
||||
"contacts_zeroHopContactAdvertFailed": "Pošiljanje kontakta ni uspelo.",
|
||||
"contacts_contactAdvertCopied": "Oglas je bil kopiran v odložišče.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiranje oglasa v odložišče je spodletelo.",
|
||||
"contacts_ShareContactZeroHop": "Deliti kontakt prek oglasa",
|
||||
"contacts_ShareContact": "Kopiraj stik v Odložišče",
|
||||
"notification_activityTitle": "Aktivnost MeshCore",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{sporočilo} =2{sporočili} few{sporočila} other{sporočil}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{sporočilo kanala} =2{sporočili kanala} few{sporočila kanala} other{sporočil kanala}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{novo vozlišče} =2{novi vozlišči} few{nova vozlišča} other{novih vozlišč}}",
|
||||
"notification_newTypeDiscovered": "Odkrito novo {contactType}",
|
||||
"notification_receivedNewMessage": "Prejeto novo sporočilo",
|
||||
"settings_gpxExportAll": "Izvozi vse kontakte v GPX",
|
||||
"settings_gpxExportContacts": "Izvoz spremljevalcev v GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Izvozi ponovljene oddajnike / strežnik sobe z lokacijo v datoteko GPX.",
|
||||
"settings_gpxExportRepeaters": "Izvoz ponoviteljev / strežnika sobe v GPX",
|
||||
"settings_gpxExportError": "Pri izvozu je prišlo do napake.",
|
||||
"settings_gpxExportRepeatersRoom": "Lokacije ponovljivca in strežnika sobe",
|
||||
"settings_gpxExportChat": "Lokacije spremljevalcev",
|
||||
"settings_gpxExportAllContacts": "Lokacije vseh stikov",
|
||||
"settings_gpxExportContactsSubtitle": "Izvozi spremljevalce z lokacijo v datoteko GPX.",
|
||||
"settings_gpxExportAllSubtitle": "Izvozi vse kontakte z lokacijo v datoteko GPX.",
|
||||
"settings_gpxExportSuccess": "Uspešno izvoz GPX datoteke.",
|
||||
"settings_gpxExportShareText": "Podatki kart izvoženi iz meshcore-open",
|
||||
"settings_gpxExportNoContacts": "Ni stikov za izvoz.",
|
||||
"settings_gpxExportNotAvailable": "Ni podprto na vašem napravi/operacijskem sistemu",
|
||||
"settings_gpxExportShareSubject": "meshcore-open izvoz podatkov GPX karte",
|
||||
"pathTrace_someHopsNoLocation": "Ena ali več hmelju manjka lokacija!",
|
||||
"map_tapToAdd": "Pritisnite na vozlišča, da jih dodate poti.",
|
||||
"map_removeLast": "Odstrani Zadnji",
|
||||
"map_runTrace": "Zaženi sledenje poti",
|
||||
"pathTrace_clearTooltip": "Počisti pot",
|
||||
"map_pathTraceCancelled": "Spremljanje poti je prekinjeno.",
|
||||
"scanner_enableBluetooth": "Omogočite Bluetooth",
|
||||
"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",
|
||||
"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_clientRepeatSubtitle": "Omogočite temu naprave, da ponavlja paketne sporočila za druge.",
|
||||
"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."
|
||||
}
|
||||
|
||||
+385
-10
@@ -1,4 +1,12 @@
|
||||
{
|
||||
"channels_channelDeleteFailed": "Det gick inte att ta bort kanalen \"{name}\"",
|
||||
"@channels_channelDeleteFailed": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@@locale": "sv",
|
||||
"appTitle": "MeshCore Open",
|
||||
"nav_contacts": "Kontakter",
|
||||
@@ -131,9 +139,6 @@
|
||||
"settings_infoContactsCount": "Kontakterantal",
|
||||
"settings_infoChannelCount": "Kanalantal",
|
||||
"settings_presets": "Fördefinierade inställningar",
|
||||
"settings_preset915Mhz": "915 MHz",
|
||||
"settings_preset868Mhz": "868 MHz",
|
||||
"settings_preset433Mhz": "433 MHz",
|
||||
"settings_frequency": "Frekvens (MHz)",
|
||||
"settings_frequencyHelper": "300,0 - 2500,0",
|
||||
"settings_frequencyInvalid": "Ogiltig frekvens (300-2500 MHz)",
|
||||
@@ -143,8 +148,6 @@
|
||||
"settings_txPower": "TX-effekt (dBm)",
|
||||
"settings_txPowerHelper": "0 - 22",
|
||||
"settings_txPowerInvalid": "Ogiltig TX-effekt (0-22 dBm)",
|
||||
"settings_longRange": "Lång räckvidd",
|
||||
"settings_fastSpeed": "Snabb hastighet",
|
||||
"settings_error": "Fel: {message}",
|
||||
"@settings_error": {
|
||||
"placeholders": {
|
||||
@@ -339,6 +342,8 @@
|
||||
"channels_publicChannel": "Allmänt kanal",
|
||||
"channels_privateChannel": "Privat kanal",
|
||||
"channels_editChannel": "Redigera kanal",
|
||||
"channels_muteChannel": "Tysta kanal",
|
||||
"channels_unmuteChannel": "Slå på ljud för kanal",
|
||||
"channels_deleteChannel": "Ta bort kanal",
|
||||
"channels_deleteChannelConfirm": "Radera \"{name}\"? Detta kan inte ångras.",
|
||||
"@channels_deleteChannelConfirm": {
|
||||
@@ -604,6 +609,18 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_openLink": "Öppna länk?",
|
||||
"chat_openLinkConfirmation": "Vill du öppna den här länken i din webbläsare?",
|
||||
"chat_open": "Öppna",
|
||||
"chat_couldNotOpenLink": "Kunde inte öppna länken: {url}",
|
||||
"@chat_couldNotOpenLink": {
|
||||
"placeholders": {
|
||||
"url": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"chat_invalidLink": "Ogiltigt länkformat",
|
||||
"map_title": "Nodkarta",
|
||||
"map_noNodesWithLocation": "Inga noder med platsinformation",
|
||||
"map_nodesNeedGps": "Noder måste dela sina GPS-koordinater\nför att visas på kartan",
|
||||
@@ -1344,12 +1361,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_neighbours": "Grannar",
|
||||
"repeater_neighboursSubtitle": "Visa noll hoppgrannar.",
|
||||
"repeater_neighbors": "Grannar",
|
||||
"repeater_neighborsSubtitle": "Visa noll hoppgrannar.",
|
||||
"neighbors_receivedData": "Mottagna grannars data",
|
||||
"neighbors_requestTimedOut": "Grannar begär tidsinställd utskick.",
|
||||
"neighbors_errorLoading": "Fel vid inläsning av grannar: {error}",
|
||||
"neighbors_repeatersNeighbours": "Upprepar grannar",
|
||||
"neighbors_repeatersNeighbors": "Upprepar grannar",
|
||||
"neighbors_noData": "Inga grannuppgifter finns tillgängliga.",
|
||||
"channels_createPrivateChannel": "Skapa en privat kanal",
|
||||
"channels_joinPrivateChannel": "Gå med i en Privat Kanal",
|
||||
@@ -1473,7 +1490,9 @@
|
||||
"community_deleteChannelsWarning": "Detta kommer också att radera {count} kanal/kanaler och deras meddelanden.",
|
||||
"@community_deleteChannelsWarning": {
|
||||
"placeholders": {
|
||||
"count": {"type": "int"}
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_deleted": "Lämnade community \"{name}\"",
|
||||
@@ -1484,5 +1503,361 @@
|
||||
"community_regularHashtagDesc": "Offentlig hashtag (alla kan gå med)",
|
||||
"community_communityHashtagDesc": "Endast för medlemmar",
|
||||
"community_forCommunity": "För {name}",
|
||||
"community_communityHashtag": "Community Hashtag"
|
||||
"community_communityHashtag": "Community Hashtag",
|
||||
"@community_regenerateSecretConfirm": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretRegenerated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_secretUpdated": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@community_scanToUpdateSecret": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"community_regenerate": "Regenerera",
|
||||
"community_regenerateSecretConfirm": "Regenerera den hemliga nyckeln för \"{name}\"? Alla medlemmar måste scanna den nya QR-koden för att fortsätta kommunicera.",
|
||||
"community_secretRegenerated": "Lösenord återskapad för \"{name}\"",
|
||||
"community_regenerateSecret": "Regenerera hemlig kod",
|
||||
"community_scanToUpdateSecret": "Skanna den nya QR-koden för att uppdatera hemligheten för \"{name}\"",
|
||||
"community_secretUpdated": "Hemlighet uppdaterad för \"{name}\"",
|
||||
"community_updateSecret": "Uppdatera hemlighet",
|
||||
"@contacts_pathTraceTo": {
|
||||
"placeholders": {
|
||||
"name": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"pathTrace_you": "Du",
|
||||
"pathTrace_failed": "Sökvägsföljning misslyckades.",
|
||||
"pathTrace_notAvailable": "Path trace ej tillgänglig.",
|
||||
"pathTrace_refreshTooltip": "Uppdatera Path Trace",
|
||||
"contacts_pathTrace": "Path Trace",
|
||||
"contacts_ping": "Ping",
|
||||
"contacts_repeaterPathTrace": "Vägspårning till repeater",
|
||||
"contacts_repeaterPing": "Ping-repeater",
|
||||
"contacts_roomPathTrace": "Vägspårning till rumserver",
|
||||
"contacts_roomPing": "Ping rumsserver",
|
||||
"contacts_chatTraceRoute": "Spåra rutt",
|
||||
"contacts_pathTraceTo": "Spåra rutt till {name}",
|
||||
"contacts_clipboardEmpty": "Urklipp är tomt.",
|
||||
"appSettings_languageRu": "Ryska",
|
||||
"contacts_contactImportFailed": "Kontakt kunde inte importeras.",
|
||||
"contacts_zeroHopAdvert": "Reklam med nollhopp",
|
||||
"contacts_floodAdvert": "Översvämningsannons",
|
||||
"contacts_copyAdvertToClipboard": "Kopiera annons till urklipp",
|
||||
"contacts_invalidAdvertFormat": "Ogiltiga kontaktuppgifter",
|
||||
"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_contactImported": "Kontakt har importerats.",
|
||||
"contacts_zeroHopContactAdvertSent": "Skickat kontakt via annons.",
|
||||
"contacts_contactAdvertCopied": "Annons kopierad till Urklipp.",
|
||||
"contacts_contactAdvertCopyFailed": "Kopiering av annons till Urklipp misslyckades.",
|
||||
"contacts_ShareContact": "Kopiera kontakt till Urklipp",
|
||||
"contacts_zeroHopContactAdvertFailed": "Misslyckades med att skicka kontakt.",
|
||||
"contacts_ShareContactZeroHop": "Dela kontakt via annons",
|
||||
"notification_activityTitle": "MeshCore Aktivitet",
|
||||
"notification_messagesCount": "{count} {count, plural, =1{meddelande} other{meddelanden}}",
|
||||
"notification_channelMessagesCount": "{count} {count, plural, =1{kanalmeddelande} other{kanalmeddelanden}}",
|
||||
"notification_newNodesCount": "{count} {count, plural, =1{ny nod} other{nya noder}}",
|
||||
"notification_newTypeDiscovered": "Ny {contactType} upptäckt",
|
||||
"notification_receivedNewMessage": "Nytt meddelande mottaget",
|
||||
"settings_gpxExportAll": "Exportera alla kontakter till GPX",
|
||||
"settings_gpxExportRepeatersSubtitle": "Exporterar repeater / roomserver med plats till GPX-fil.",
|
||||
"settings_gpxExportSuccess": "Har exporterat GPX-fil med framgång",
|
||||
"settings_gpxExportNoContacts": "Inga kontakter att exportera.",
|
||||
"settings_gpxExportNotAvailable": "Stöds inte på din enhet/operativsystem",
|
||||
"settings_gpxExportRepeatersRoom": "Repeater- och rumsserverplatser",
|
||||
"settings_gpxExportRepeaters": "Exportera repeater / rumsservrar till GPX",
|
||||
"settings_gpxExportAllSubtitle": "Exporterar alla kontakter med en plats till GPX-fil.",
|
||||
"settings_gpxExportContacts": "Exportera följeslagare till GPX",
|
||||
"settings_gpxExportContactsSubtitle": "Exporterar följeslagare med en plats till GPX-fil.",
|
||||
"settings_gpxExportChat": "Medhjälparplatser",
|
||||
"settings_gpxExportError": "Det uppstod ett fel när data exporterades.",
|
||||
"settings_gpxExportAllContacts": "Alla kontakters platser",
|
||||
"settings_gpxExportShareSubject": "meshcore-open export av GPX-kartdata",
|
||||
"settings_gpxExportShareText": "Kartdata exporterad från meshcore-open",
|
||||
"pathTrace_someHopsNoLocation": "En eller flera av humlen saknar en plats!",
|
||||
"pathTrace_clearTooltip": "Rensa väg",
|
||||
"map_pathTraceCancelled": "Sökvägsspårning avbruten.",
|
||||
"map_runTrace": "Kör spårsökning",
|
||||
"map_tapToAdd": "Tryck på noder för att lägga till dem i banan.",
|
||||
"map_removeLast": "Ta bort sista",
|
||||
"scanner_enableBluetooth": "Aktivera Bluetooth",
|
||||
"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",
|
||||
"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_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_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."
|
||||
}
|
||||
|
||||
+1863
File diff suppressed because it is too large
Load Diff
+849
-469
File diff suppressed because it is too large
Load Diff
+68
-13
@@ -1,8 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'services/storage_service.dart';
|
||||
@@ -14,6 +18,7 @@ import 'services/ble_debug_log_service.dart';
|
||||
import 'services/app_debug_log_service.dart';
|
||||
import 'services/background_service.dart';
|
||||
import 'services/map_tile_cache_service.dart';
|
||||
import 'services/chat_text_scale_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
|
||||
@@ -33,6 +38,7 @@ void main() async {
|
||||
final appDebugLogService = AppDebugLogService();
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
|
||||
// Load settings
|
||||
await appSettingsService.loadSettings();
|
||||
@@ -47,6 +53,9 @@ void main() async {
|
||||
final notificationService = NotificationService();
|
||||
await notificationService.initialize();
|
||||
await backgroundService.initialize();
|
||||
_registerThirdPartyLicenses();
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
|
||||
// Wire up connector with services
|
||||
connector.initialize(
|
||||
@@ -60,21 +69,46 @@ void main() async {
|
||||
|
||||
await connector.loadContactCache();
|
||||
await connector.loadChannelSettings();
|
||||
await connector.loadCachedChannels();
|
||||
|
||||
// Load persisted channel messages
|
||||
await connector.loadAllChannelMessages();
|
||||
await connector.loadUnreadState();
|
||||
|
||||
runApp(MeshCoreApp(
|
||||
connector: connector,
|
||||
retryService: retryService,
|
||||
pathHistoryService: pathHistoryService,
|
||||
storage: storage,
|
||||
appSettingsService: appSettingsService,
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
));
|
||||
runApp(
|
||||
MeshCoreApp(
|
||||
connector: connector,
|
||||
retryService: retryService,
|
||||
pathHistoryService: pathHistoryService,
|
||||
storage: storage,
|
||||
appSettingsService: appSettingsService,
|
||||
bleDebugLogService: bleDebugLogService,
|
||||
appDebugLogService: appDebugLogService,
|
||||
mapTileCacheService: mapTileCacheService,
|
||||
chatTextScaleService: chatTextScaleService,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _registerThirdPartyLicenses() {
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
<String>['Open-Meteo Elevation API Data'],
|
||||
'''
|
||||
Data used by LOS elevation lookups is provided by Open-Meteo.
|
||||
|
||||
Open-Meteo terms and attribution:
|
||||
https://open-meteo.com/en/terms
|
||||
|
||||
Elevation API:
|
||||
https://open-meteo.com/en/docs/elevation-api
|
||||
|
||||
Attribution license reference:
|
||||
Creative Commons Attribution 4.0 International (CC BY 4.0)
|
||||
https://creativecommons.org/licenses/by/4.0/
|
||||
''',
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreApp extends StatelessWidget {
|
||||
@@ -86,6 +120,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final BleDebugLogService bleDebugLogService;
|
||||
final AppDebugLogService appDebugLogService;
|
||||
final MapTileCacheService mapTileCacheService;
|
||||
final ChatTextScaleService chatTextScaleService;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -97,6 +132,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.bleDebugLogService,
|
||||
required this.appDebugLogService,
|
||||
required this.mapTileCacheService,
|
||||
required this.chatTextScaleService,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -109,6 +145,7 @@ class MeshCoreApp extends StatelessWidget {
|
||||
ChangeNotifierProvider.value(value: appSettingsService),
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
],
|
||||
@@ -124,10 +161,15 @@ class MeshCoreApp extends StatelessWidget {
|
||||
GlobalCupertinoLocalizations.delegate,
|
||||
],
|
||||
supportedLocales: AppLocalizations.supportedLocales,
|
||||
locale: _localeFromSetting(settingsService.settings.languageOverride),
|
||||
locale: _localeFromSetting(
|
||||
settingsService.settings.languageOverride,
|
||||
),
|
||||
theme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
darkTheme: ThemeData(
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
@@ -135,9 +177,22 @@ class MeshCoreApp extends StatelessWidget {
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
),
|
||||
themeMode: _themeModeFromSetting(settingsService.settings.themeMode),
|
||||
home: const ScannerScreen(),
|
||||
themeMode: _themeModeFromSetting(
|
||||
settingsService.settings.themeMode,
|
||||
),
|
||||
builder: (context, child) {
|
||||
// Update notification service with resolved locale
|
||||
final locale = Localizations.localeOf(context);
|
||||
NotificationService().setLocale(locale);
|
||||
return child ?? const SizedBox.shrink();
|
||||
},
|
||||
home: (PlatformInfo.isWeb && !PlatformInfo.isChrome)
|
||||
? const ChromeRequiredScreen()
|
||||
: const ScannerScreen(),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -1,3 +1,16 @@
|
||||
enum UnitSystem { metric, imperial }
|
||||
|
||||
extension UnitSystemValue on UnitSystem {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case UnitSystem.imperial:
|
||||
return 'imperial';
|
||||
case UnitSystem.metric:
|
||||
return 'metric';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class AppSettings {
|
||||
static const Object _unset = Object();
|
||||
|
||||
@@ -9,6 +22,8 @@ class AppSettings {
|
||||
final bool mapKeyPrefixEnabled;
|
||||
final String mapKeyPrefix;
|
||||
final bool mapShowMarkers;
|
||||
final bool mapShowGuessedLocations;
|
||||
final bool enableMessageTracing;
|
||||
final Map<String, double>? mapCacheBounds;
|
||||
final int mapCacheMinZoom;
|
||||
final int mapCacheMaxZoom;
|
||||
@@ -21,6 +36,9 @@ class AppSettings {
|
||||
final String? languageOverride; // null = system default
|
||||
final bool appDebugLogEnabled;
|
||||
final Map<String, String> batteryChemistryByDeviceId;
|
||||
final Map<String, String> batteryChemistryByRepeaterId;
|
||||
final UnitSystem unitSystem;
|
||||
final Set<String> mutedChannels;
|
||||
|
||||
AppSettings({
|
||||
this.clearPathOnMaxRetry = false,
|
||||
@@ -31,6 +49,8 @@ class AppSettings {
|
||||
this.mapKeyPrefixEnabled = false,
|
||||
this.mapKeyPrefix = '',
|
||||
this.mapShowMarkers = true,
|
||||
this.mapShowGuessedLocations = true,
|
||||
this.enableMessageTracing = false,
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
this.mapCacheMaxZoom = 15,
|
||||
@@ -43,7 +63,12 @@ class AppSettings {
|
||||
this.languageOverride,
|
||||
this.appDebugLogEnabled = false,
|
||||
Map<String, String>? batteryChemistryByDeviceId,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {};
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
this.unitSystem = UnitSystem.metric,
|
||||
Set<String>? mutedChannels,
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {};
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
@@ -55,6 +80,8 @@ class AppSettings {
|
||||
'map_key_prefix_enabled': mapKeyPrefixEnabled,
|
||||
'map_key_prefix': mapKeyPrefix,
|
||||
'map_show_markers': mapShowMarkers,
|
||||
'map_show_guessed_locations': mapShowGuessedLocations,
|
||||
'enable_message_tracing': enableMessageTracing,
|
||||
'map_cache_bounds': mapCacheBounds,
|
||||
'map_cache_min_zoom': mapCacheMinZoom,
|
||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||
@@ -67,22 +94,36 @@ class AppSettings {
|
||||
'language_override': languageOverride,
|
||||
'app_debug_log_enabled': appDebugLogEnabled,
|
||||
'battery_chemistry_by_device_id': batteryChemistryByDeviceId,
|
||||
'battery_chemistry_by_repeater_id': batteryChemistryByRepeaterId,
|
||||
'unit_system': unitSystem.value,
|
||||
'muted_channels': mutedChannels.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
factory AppSettings.fromJson(Map<String, dynamic> json) {
|
||||
UnitSystem parseUnitSystem(dynamic value) {
|
||||
if (value is String && value.toLowerCase() == 'imperial') {
|
||||
return UnitSystem.imperial;
|
||||
}
|
||||
return UnitSystem.metric;
|
||||
}
|
||||
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: json['clear_path_on_max_retry'] as bool? ?? false,
|
||||
mapShowRepeaters: json['map_show_repeaters'] as bool? ?? true,
|
||||
mapShowChatNodes: json['map_show_chat_nodes'] as bool? ?? true,
|
||||
mapShowOtherNodes: json['map_show_other_nodes'] as bool? ?? true,
|
||||
mapTimeFilterHours: (json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||
mapTimeFilterHours:
|
||||
(json['map_time_filter_hours'] as num?)?.toDouble() ?? 0,
|
||||
mapKeyPrefixEnabled: json['map_key_prefix_enabled'] as bool? ?? false,
|
||||
mapKeyPrefix: json['map_key_prefix'] as String? ?? '',
|
||||
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(
|
||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||
),
|
||||
(key, value) => MapEntry(key.toString(), (value as num).toDouble()),
|
||||
),
|
||||
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
||||
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
||||
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
||||
@@ -90,14 +131,27 @@ class AppSettings {
|
||||
notifyOnNewChannelMessage:
|
||||
json['notify_on_new_channel_message'] as bool? ?? true,
|
||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||
autoRouteRotationEnabled: json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||
autoRouteRotationEnabled:
|
||||
json['auto_route_rotation_enabled'] as bool? ?? false,
|
||||
themeMode: json['theme_mode'] as String? ?? 'system',
|
||||
languageOverride: json['language_override'] as String?,
|
||||
appDebugLogEnabled: json['app_debug_log_enabled'] as bool? ?? false,
|
||||
batteryChemistryByDeviceId: (json['battery_chemistry_by_device_id'] as Map?)?.map(
|
||||
batteryChemistryByDeviceId:
|
||||
(json['battery_chemistry_by_device_id'] as Map?)?.map(
|
||||
(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()) ??
|
||||
{},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -110,6 +164,8 @@ class AppSettings {
|
||||
bool? mapKeyPrefixEnabled,
|
||||
String? mapKeyPrefix,
|
||||
bool? mapShowMarkers,
|
||||
bool? mapShowGuessedLocations,
|
||||
bool? enableMessageTracing,
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
int? mapCacheMaxZoom,
|
||||
@@ -122,6 +178,9 @@ class AppSettings {
|
||||
Object? languageOverride = _unset,
|
||||
bool? appDebugLogEnabled,
|
||||
Map<String, String>? batteryChemistryByDeviceId,
|
||||
Map<String, String>? batteryChemistryByRepeaterId,
|
||||
UnitSystem? unitSystem,
|
||||
Set<String>? mutedChannels,
|
||||
}) {
|
||||
return AppSettings(
|
||||
clearPathOnMaxRetry: clearPathOnMaxRetry ?? this.clearPathOnMaxRetry,
|
||||
@@ -132,8 +191,12 @@ class AppSettings {
|
||||
mapKeyPrefixEnabled: mapKeyPrefixEnabled ?? this.mapKeyPrefixEnabled,
|
||||
mapKeyPrefix: mapKeyPrefix ?? this.mapKeyPrefix,
|
||||
mapShowMarkers: mapShowMarkers ?? this.mapShowMarkers,
|
||||
mapCacheBounds:
|
||||
mapCacheBounds == _unset ? this.mapCacheBounds : mapCacheBounds as Map<String, double>?,
|
||||
mapShowGuessedLocations:
|
||||
mapShowGuessedLocations ?? this.mapShowGuessedLocations,
|
||||
enableMessageTracing: enableMessageTracing ?? this.enableMessageTracing,
|
||||
mapCacheBounds: mapCacheBounds == _unset
|
||||
? this.mapCacheBounds
|
||||
: mapCacheBounds as Map<String, double>?,
|
||||
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
||||
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
@@ -141,12 +204,19 @@ class AppSettings {
|
||||
notifyOnNewChannelMessage:
|
||||
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
|
||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||
autoRouteRotationEnabled: autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||
autoRouteRotationEnabled:
|
||||
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||
themeMode: themeMode ?? this.themeMode,
|
||||
languageOverride:
|
||||
languageOverride == _unset ? this.languageOverride : languageOverride as String?,
|
||||
languageOverride: languageOverride == _unset
|
||||
? this.languageOverride
|
||||
: languageOverride as String?,
|
||||
appDebugLogEnabled: appDebugLogEnabled ?? this.appDebugLogEnabled,
|
||||
batteryChemistryByDeviceId: batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||
batteryChemistryByDeviceId:
|
||||
batteryChemistryByDeviceId ?? this.batteryChemistryByDeviceId,
|
||||
batteryChemistryByRepeaterId:
|
||||
batteryChemistryByRepeaterId ?? this.batteryChemistryByRepeaterId,
|
||||
unitSystem: unitSystem ?? this.unitSystem,
|
||||
mutedChannels: mutedChannels ?? this.mutedChannels,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,11 +9,13 @@ class Channel {
|
||||
final int index;
|
||||
final String name;
|
||||
final Uint8List psk; // 16 bytes
|
||||
int unreadCount;
|
||||
|
||||
Channel({
|
||||
required this.index,
|
||||
required this.name,
|
||||
required this.psk,
|
||||
this.unreadCount = 0,
|
||||
});
|
||||
|
||||
String get pskHex => _bytesToHex(psk);
|
||||
@@ -39,11 +41,7 @@ class Channel {
|
||||
}
|
||||
|
||||
static Channel empty(int index) {
|
||||
return Channel(
|
||||
index: index,
|
||||
name: '',
|
||||
psk: Uint8List(16),
|
||||
);
|
||||
return Channel(index: index, name: '', psk: Uint8List(16));
|
||||
}
|
||||
|
||||
static Channel fromHex(int index, String name, String pskHex) {
|
||||
|
||||
@@ -59,15 +59,18 @@ class ChannelMessage {
|
||||
this.replyToSenderName,
|
||||
this.replyToText,
|
||||
Map<String, int>? reactions,
|
||||
}) : messageId = messageId ?? '${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
|
||||
reactions = reactions ?? {},
|
||||
pathBytes = pathBytes ?? Uint8List(0),
|
||||
pathVariants = _mergePathVariants(
|
||||
pathBytes ?? Uint8List(0),
|
||||
pathVariants,
|
||||
);
|
||||
}) : messageId =
|
||||
messageId ??
|
||||
'${timestamp.millisecondsSinceEpoch}_${senderName.hashCode}_${text.hashCode}',
|
||||
reactions = reactions ?? {},
|
||||
pathBytes = pathBytes ?? Uint8List(0),
|
||||
pathVariants = _mergePathVariants(
|
||||
pathBytes ?? Uint8List(0),
|
||||
pathVariants,
|
||||
);
|
||||
|
||||
String? get senderKeyHex => senderKey != null ? pubKeyToHex(senderKey!) : null;
|
||||
String? get senderKeyHex =>
|
||||
senderKey != null ? pubKeyToHex(senderKey!) : null;
|
||||
|
||||
ChannelMessage copyWith({
|
||||
ChannelMessageStatus? status,
|
||||
@@ -125,8 +128,10 @@ class ChannelMessage {
|
||||
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) {
|
||||
cursor < data.length &&
|
||||
(data[cursor] == txtTypePlain || data[cursor] == txtTypeCliData);
|
||||
if ((hasPathBytesFlag || (canFitPath && !hasValidTxtType)) &&
|
||||
canFitPath) {
|
||||
pathBytes = Uint8List.fromList(data.sublist(cursor, cursor + pathLen));
|
||||
cursor += pathLen;
|
||||
}
|
||||
@@ -162,7 +167,8 @@ class ChannelMessage {
|
||||
final potentialSender = text.substring(0, colonIndex);
|
||||
if (!RegExp(r'[:\[\]]').hasMatch(potentialSender)) {
|
||||
senderName = potentialSender;
|
||||
final offset = (colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
final offset =
|
||||
(colonIndex + 1 < text.length && text[colonIndex + 1] == ' ')
|
||||
? colonIndex + 2
|
||||
: colonIndex + 1;
|
||||
actualText = text.substring(offset);
|
||||
@@ -184,7 +190,11 @@ class ChannelMessage {
|
||||
);
|
||||
}
|
||||
|
||||
static ChannelMessage outgoing(String text, String senderName, int channelIndex) {
|
||||
static ChannelMessage outgoing(
|
||||
String text,
|
||||
String senderName,
|
||||
int channelIndex,
|
||||
) {
|
||||
return ChannelMessage(
|
||||
senderKey: null,
|
||||
senderName: senderName,
|
||||
@@ -249,8 +259,5 @@ class ReplyInfo {
|
||||
final String mentionedNode;
|
||||
final String actualMessage;
|
||||
|
||||
ReplyInfo({
|
||||
required this.mentionedNode,
|
||||
required this.actualMessage,
|
||||
});
|
||||
ReplyInfo({required this.mentionedNode, required this.actualMessage});
|
||||
}
|
||||
|
||||
@@ -34,10 +34,7 @@ class Community {
|
||||
}) : hashtagChannels = hashtagChannels ?? [];
|
||||
|
||||
/// Generate a new community with a random 32-byte secret
|
||||
factory Community.create({
|
||||
required String id,
|
||||
required String name,
|
||||
}) {
|
||||
factory Community.create({required String id, required String name}) {
|
||||
final random = Random.secure();
|
||||
final secret = Uint8List(32);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
@@ -84,7 +81,8 @@ class Community {
|
||||
name: json['name'] as String,
|
||||
secret: base64Decode(json['secret'] as String),
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(json['created_at'] as int),
|
||||
hashtagChannels: (json['hashtag_channels'] as List<dynamic>?)
|
||||
hashtagChannels:
|
||||
(json['hashtag_channels'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
[],
|
||||
@@ -234,9 +232,7 @@ class Community {
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
identical(this, other) ||
|
||||
other is Community &&
|
||||
runtimeType == other.runtimeType &&
|
||||
id == other.id;
|
||||
other is Community && runtimeType == other.runtimeType && id == other.id;
|
||||
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
|
||||
+98
-38
@@ -5,9 +5,11 @@ class Contact {
|
||||
final Uint8List publicKey;
|
||||
final String name;
|
||||
final int type;
|
||||
final int flags;
|
||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||
final Uint8List path; // Path bytes from device
|
||||
final int? pathOverride; // User's path override: -1 = force flood, null = auto
|
||||
final int?
|
||||
pathOverride; // User's path override: -1 = force flood, null = auto
|
||||
final Uint8List? pathOverrideBytes; // User's path override bytes
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
@@ -18,6 +20,7 @@ class Contact {
|
||||
required this.publicKey,
|
||||
required this.name,
|
||||
required this.type,
|
||||
this.flags = 0,
|
||||
required this.pathLength,
|
||||
required this.path,
|
||||
this.pathOverride,
|
||||
@@ -57,11 +60,13 @@ class Contact {
|
||||
}
|
||||
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
bool get isFavorite => (flags & contactFlagFavorite) != 0;
|
||||
|
||||
Contact copyWith({
|
||||
Uint8List? publicKey,
|
||||
String? name,
|
||||
int? type,
|
||||
int? flags,
|
||||
int? pathLength,
|
||||
Uint8List? path,
|
||||
int? pathOverride,
|
||||
@@ -76,10 +81,15 @@ class Contact {
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
flags: flags ?? this.flags,
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
path: path ?? this.path,
|
||||
pathOverride: clearPathOverride ? null : (pathOverride ?? this.pathOverride),
|
||||
pathOverrideBytes: clearPathOverride ? null : (pathOverrideBytes ?? this.pathOverrideBytes),
|
||||
pathOverride: clearPathOverride
|
||||
? null
|
||||
: (pathOverride ?? this.pathOverride),
|
||||
pathOverrideBytes: clearPathOverride
|
||||
? null
|
||||
: (pathOverrideBytes ?? this.pathOverrideBytes),
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
@@ -93,15 +103,59 @@ class Contact {
|
||||
final parts = <String>[];
|
||||
final groupSize = pathHashSize;
|
||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||
final end = (i + groupSize) <= pathBytes.length ? (i + groupSize) : pathBytes.length;
|
||||
final end = (i + groupSize) <= pathBytes.length
|
||||
? (i + groupSize)
|
||||
: pathBytes.length;
|
||||
final chunk = pathBytes.sublist(i, end);
|
||||
parts.add(
|
||||
chunk.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()).join(),
|
||||
chunk
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(),
|
||||
);
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
String get shortPubKeyHex {
|
||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||
}
|
||||
|
||||
Uint8List? get traceRouteBytes {
|
||||
final pathBytes = _pathBytesForDisplay;
|
||||
Uint8List? traceBytes;
|
||||
|
||||
if (pathBytes.isEmpty) {
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = publicKey[0];
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
if (type == advTypeRepeater || type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = publicKey[0];
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? null : pathBytes;
|
||||
}
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
Uint8List get _pathBytesForDisplay {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return Uint8List(0);
|
||||
@@ -111,43 +165,49 @@ class Contact {
|
||||
}
|
||||
|
||||
static Contact? fromFrame(Uint8List data) {
|
||||
if (data.length < contactFrameSize) return null;
|
||||
if (data.isEmpty) return null;
|
||||
if (data[0] != respCodeContact) return null;
|
||||
try {
|
||||
final pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
final flags = data[contactFlagsOffset];
|
||||
final pathLen = data[contactPathLenOffset].toSigned(8);
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
: 0;
|
||||
final pathBytes = safePathLen > 0
|
||||
? Uint8List.fromList(
|
||||
data.sublist(contactPathOffset, contactPathOffset + safePathLen),
|
||||
)
|
||||
: Uint8List(0);
|
||||
final name = readCString(data, contactNameOffset, maxNameSize);
|
||||
final lastmod = readUint32LE(data, contactLastModOffset);
|
||||
|
||||
final pubKey = Uint8List.fromList(
|
||||
data.sublist(contactPubKeyOffset, contactPubKeyOffset + pubKeySize),
|
||||
);
|
||||
final type = data[contactTypeOffset];
|
||||
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 latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
if (latRaw != 0 || lonRaw != 0) {
|
||||
lat = latRaw / 1e6;
|
||||
lon = lonRaw / 1e6;
|
||||
}
|
||||
|
||||
double? lat, lon;
|
||||
final latRaw = readInt32LE(data, contactLatOffset);
|
||||
final lonRaw = readInt32LE(data, contactLonOffset);
|
||||
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,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
);
|
||||
} catch (e) {
|
||||
// If parsing fails, return null
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact(
|
||||
publicKey: pubKey,
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
pathLength: pathLen,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
lastSeen: DateTime.fromMillisecondsSinceEpoch(lastmod * 1000),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -2,15 +2,9 @@ class ContactGroup {
|
||||
final String name;
|
||||
final List<String> memberKeys;
|
||||
|
||||
const ContactGroup({
|
||||
required this.name,
|
||||
required this.memberKeys,
|
||||
});
|
||||
const ContactGroup({required this.name, required this.memberKeys});
|
||||
|
||||
ContactGroup copyWith({
|
||||
String? name,
|
||||
List<String>? memberKeys,
|
||||
}) {
|
||||
ContactGroup copyWith({String? name, List<String>? memberKeys}) {
|
||||
return ContactGroup(
|
||||
name: name ?? this.name,
|
||||
memberKeys: memberKeys ?? List<String>.from(this.memberKeys),
|
||||
@@ -18,16 +12,12 @@ class ContactGroup {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'name': name,
|
||||
'members': memberKeys,
|
||||
};
|
||||
return {'name': name, 'members': memberKeys};
|
||||
}
|
||||
|
||||
factory ContactGroup.fromJson(Map<String, dynamic> json) {
|
||||
final members = (json['members'] as List?)
|
||||
?.map((value) => value.toString())
|
||||
.toList() ??
|
||||
final members =
|
||||
(json['members'] as List?)?.map((value) => value.toString()).toList() ??
|
||||
<String>[];
|
||||
return ContactGroup(
|
||||
name: json['name'] as String? ?? '',
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
import 'dart:typed_data';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
|
||||
class DiscoveryContact {
|
||||
final Uint8List rawPacket;
|
||||
final Uint8List publicKey;
|
||||
final String name;
|
||||
final int type;
|
||||
final int pathLength; // -1 = flood, 0+ = direct hops (from device)
|
||||
final Uint8List path; // Path bytes from device
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final DateTime lastSeen;
|
||||
|
||||
DiscoveryContact({
|
||||
required this.rawPacket,
|
||||
required this.publicKey,
|
||||
required this.name,
|
||||
required this.type,
|
||||
required this.pathLength,
|
||||
required this.path,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
required this.lastSeen,
|
||||
});
|
||||
|
||||
String get publicKeyHex => pubKeyToHex(publicKey);
|
||||
|
||||
String get typeLabel {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return 'Chat';
|
||||
case advTypeRepeater:
|
||||
return 'Repeater';
|
||||
case advTypeRoom:
|
||||
return 'Room';
|
||||
case advTypeSensor:
|
||||
return 'Sensor';
|
||||
default:
|
||||
return 'Unknown';
|
||||
}
|
||||
}
|
||||
|
||||
String get pathLabel {
|
||||
if (pathLength < 0) return 'Flood';
|
||||
if (pathLength == 0) return 'Direct';
|
||||
return '$pathLength hops';
|
||||
}
|
||||
|
||||
bool get hasLocation => latitude != null && longitude != null;
|
||||
|
||||
DiscoveryContact copyWith({
|
||||
Uint8List? rawPacket,
|
||||
Uint8List? publicKey,
|
||||
String? name,
|
||||
int? type,
|
||||
int? pathLength,
|
||||
Uint8List? path,
|
||||
double? latitude,
|
||||
double? longitude,
|
||||
DateTime? lastSeen,
|
||||
}) {
|
||||
return DiscoveryContact(
|
||||
rawPacket: rawPacket ?? this.rawPacket,
|
||||
publicKey: publicKey ?? this.publicKey,
|
||||
name: name ?? this.name,
|
||||
type: type ?? this.type,
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
path: path ?? this.path,
|
||||
latitude: latitude ?? this.latitude,
|
||||
longitude: longitude ?? this.longitude,
|
||||
lastSeen: lastSeen ?? this.lastSeen,
|
||||
);
|
||||
}
|
||||
|
||||
String get pathIdList {
|
||||
final pathBytes = path;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final parts = <String>[];
|
||||
final groupSize = pathHashSize;
|
||||
for (int i = 0; i < pathBytes.length; i += groupSize) {
|
||||
final end = (i + groupSize) <= pathBytes.length
|
||||
? (i + groupSize)
|
||||
: pathBytes.length;
|
||||
final chunk = pathBytes.sublist(i, end);
|
||||
parts.add(
|
||||
chunk
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(),
|
||||
);
|
||||
}
|
||||
return parts.join(',');
|
||||
}
|
||||
|
||||
String get shortPubKeyHex {
|
||||
return "<${publicKeyHex.substring(0, 8)}...${publicKeyHex.substring(publicKeyHex.length - 8)}>";
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is DiscoveryContact && publicKeyHex == other.publicKeyHex;
|
||||
|
||||
@override
|
||||
int get hashCode => publicKeyHex.hashCode;
|
||||
}
|
||||
@@ -43,9 +43,9 @@ class Message {
|
||||
Uint8List? pathBytes,
|
||||
Uint8List? fourByteRoomContactKey,
|
||||
Map<String, int>? reactions,
|
||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||
reactions = reactions ?? {};
|
||||
}) : pathBytes = pathBytes ?? Uint8List(0),
|
||||
fourByteRoomContactKey = fourByteRoomContactKey ?? Uint8List(0),
|
||||
reactions = reactions ?? {};
|
||||
|
||||
String get senderKeyHex => pubKeyToHex(senderKey);
|
||||
|
||||
@@ -80,7 +80,8 @@ class Message {
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
reactions: reactions ?? this.reactions,
|
||||
fourByteRoomContactKey: fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
||||
fourByteRoomContactKey:
|
||||
fourByteRoomContactKey ?? this.fourByteRoomContactKey,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -38,7 +38,8 @@ class PathRecord {
|
||||
tripTimeMs: json['trip_time_ms'] as int,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
wasFloodDiscovery: json['was_flood'] as bool,
|
||||
pathBytes: (json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
||||
pathBytes:
|
||||
(json['path_bytes'] as List?)?.map((b) => b as int).toList() ?? [],
|
||||
successCount: json['success_count'] as int? ?? 0,
|
||||
failureCount: json['failure_count'] as int? ?? 0,
|
||||
);
|
||||
@@ -65,14 +66,15 @@ class ContactPathHistory {
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'recent_paths': recentPaths.map((p) => p.toJson()).toList(),
|
||||
};
|
||||
return {'recent_paths': recentPaths.map((p) => p.toJson()).toList()};
|
||||
}
|
||||
|
||||
factory ContactPathHistory.fromJson(
|
||||
String contactPubKeyHex, Map<String, dynamic> json) {
|
||||
final pathsList = (json['recent_paths'] as List?)
|
||||
String contactPubKeyHex,
|
||||
Map<String, dynamic> json,
|
||||
) {
|
||||
final pathsList =
|
||||
(json['recent_paths'] as List?)
|
||||
?.map((p) => PathRecord.fromJson(p as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
[];
|
||||
|
||||
+183
-29
@@ -59,46 +59,200 @@ class RadioSettings {
|
||||
required this.txPowerDbm,
|
||||
});
|
||||
|
||||
// Preset configurations
|
||||
static RadioSettings get preset915MHz => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
// Regional preset configurations
|
||||
static final List<(String, RadioSettings)> presets = [
|
||||
(
|
||||
'Australia',
|
||||
RadioSettings(
|
||||
frequencyMHz: 915.8,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 916.575,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get preset868MHz => RadioSettings(
|
||||
frequencyMHz: 868.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia SA, WA, QLD',
|
||||
RadioSettings(
|
||||
frequencyMHz: 923.125,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Czech Republic',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.432,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
);
|
||||
|
||||
static RadioSettings get preset433MHz => RadioSettings(
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU 433MHz',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.650,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Long Range)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.525,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Medium Range)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.525,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'EU/UK (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'New Zealand',
|
||||
RadioSettings(
|
||||
frequencyMHz: 917.375,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'New Zealand (Narrow)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 917.375,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Portugal 433',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.375,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Portugal 869',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Switzerland',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.618,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf8,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'USA Arizona',
|
||||
RadioSettings(
|
||||
frequencyMHz: 908.205,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'USA/Canada',
|
||||
RadioSettings(
|
||||
frequencyMHz: 910.525,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Vietnam',
|
||||
RadioSettings(
|
||||
frequencyMHz: 920.250,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
// Off-grid repeat presets (valid client_repeat frequencies)
|
||||
(
|
||||
'Off-Grid 433',
|
||||
RadioSettings(
|
||||
frequencyMHz: 433.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get presetLongRange => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf12,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
|
||||
static RadioSettings get presetFastSpeed => RadioSettings(
|
||||
frequencyMHz: 915.0,
|
||||
bandwidth: LoRaBandwidth.bw500,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf7,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Off-Grid 869',
|
||||
RadioSettings(
|
||||
frequencyMHz: 869.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 14,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Off-Grid 918',
|
||||
RadioSettings(
|
||||
frequencyMHz: 918.0,
|
||||
bandwidth: LoRaBandwidth.bw250,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf11,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
);
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
int get frequencyHz => (frequencyMHz * 1000).round();
|
||||
int get bandwidthHz => bandwidth.hz;
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class AppDebugLogScreen extends StatelessWidget {
|
||||
const AppDebugLogScreen({super.key});
|
||||
@@ -17,7 +18,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.debugLog_appTitle),
|
||||
title: AdaptiveAppBarTitle(context.l10n.debugLog_appTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
@@ -26,8 +27,10 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
onPressed: hasEntries
|
||||
? () async {
|
||||
final text = entries
|
||||
.map((entry) =>
|
||||
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}')
|
||||
.map(
|
||||
(entry) =>
|
||||
'[${entry.formattedTime}] [${entry.levelLabel}] [${entry.tag}] ${entry.message}',
|
||||
)
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
@@ -53,7 +56,7 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
child: hasEntries
|
||||
? ListView.separated(
|
||||
itemCount: entries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final entry = entries[index];
|
||||
return ListTile(
|
||||
@@ -61,11 +64,17 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
leading: _buildLevelIcon(entry.level),
|
||||
title: Text(
|
||||
'[${entry.tag}] ${entry.message}',
|
||||
style: const TextStyle(fontSize: 12, fontFamily: 'monospace'),
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
entry.formattedTime,
|
||||
style: TextStyle(fontSize: 10, color: Colors.grey[600]),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -74,16 +83,26 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(Icons.bug_report_outlined, size: 64, color: Colors.grey[400]),
|
||||
Icon(
|
||||
Icons.bug_report_outlined,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
context.l10n.debugLog_noEntries,
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
context.l10n.debugLog_enableInSettings,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[500]),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[500],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -99,7 +118,11 @@ class AppDebugLogScreen extends StatelessWidget {
|
||||
case AppDebugLogLevel.info:
|
||||
return const Icon(Icons.info_outline, size: 18, color: Colors.blue);
|
||||
case AppDebugLogLevel.warning:
|
||||
return const Icon(Icons.warning_amber_outlined, size: 18, color: Colors.orange);
|
||||
return const Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 18,
|
||||
color: Colors.orange,
|
||||
);
|
||||
case AppDebugLogLevel.error:
|
||||
return const Icon(Icons.error_outline, size: 18, color: Colors.red);
|
||||
}
|
||||
|
||||
@@ -3,8 +3,10 @@ import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
class AppSettingsScreen extends StatelessWidget {
|
||||
@@ -14,7 +16,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.appSettings_title),
|
||||
title: AdaptiveAppBarTitle(context.l10n.appSettings_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: SafeArea(
|
||||
@@ -43,7 +45,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAppearanceCard(BuildContext context, AppSettingsService settingsService) {
|
||||
Widget _buildAppearanceCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -58,7 +63,9 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.brightness_6_outlined),
|
||||
title: Text(context.l10n.appSettings_theme),
|
||||
subtitle: Text(_themeModeLabel(context, settingsService.settings.themeMode)),
|
||||
subtitle: Text(
|
||||
_themeModeLabel(context, settingsService.settings.themeMode),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showThemeModeDialog(context, settingsService),
|
||||
),
|
||||
@@ -66,16 +73,36 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
ListTile(
|
||||
leading: const Icon(Icons.language_outlined),
|
||||
title: Text(context.l10n.appSettings_language),
|
||||
subtitle: Text(_languageLabel(context, settingsService.settings.languageOverride)),
|
||||
subtitle: Text(
|
||||
_languageLabel(
|
||||
context,
|
||||
settingsService.settings.languageOverride,
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
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);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNotificationsCard(BuildContext context, AppSettingsService settingsService) {
|
||||
Widget _buildNotificationsCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -90,17 +117,22 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.notifications_outlined),
|
||||
title: Text(context.l10n.appSettings_enableNotifications),
|
||||
subtitle: Text(context.l10n.appSettings_enableNotificationsSubtitle),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_enableNotificationsSubtitle,
|
||||
),
|
||||
value: settingsService.settings.notificationsEnabled,
|
||||
onChanged: (value) async {
|
||||
if (value) {
|
||||
// Request permission when enabling
|
||||
final granted = await NotificationService().requestPermissions();
|
||||
final granted = await NotificationService()
|
||||
.requestPermissions();
|
||||
if (!granted) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.appSettings_notificationPermissionDenied),
|
||||
content: Text(
|
||||
context.l10n.appSettings_notificationPermissionDenied,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -113,9 +145,11 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled),
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_notificationsEnabled
|
||||
: context.l10n.appSettings_notificationsDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -126,18 +160,24 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.message_outlined,
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_messageNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_messageNotificationsSubtitle,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewMessage,
|
||||
@@ -151,18 +191,24 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.forum_outlined,
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_channelMessageNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_channelMessageNotificationsSubtitle,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewChannelMessage,
|
||||
@@ -176,18 +222,24 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: Icon(
|
||||
Icons.cell_tower,
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
title: Text(
|
||||
context.l10n.appSettings_advertisementNotifications,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_advertisementNotificationsSubtitle,
|
||||
style: TextStyle(
|
||||
color: settingsService.settings.notificationsEnabled ? null : Colors.grey,
|
||||
color: settingsService.settings.notificationsEnabled
|
||||
? null
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
value: settingsService.settings.notifyOnNewAdvert,
|
||||
@@ -202,7 +254,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagingCard(BuildContext context, AppSettingsService settingsService) {
|
||||
Widget _buildMessagingCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -217,15 +272,19 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
SwitchListTile(
|
||||
secondary: const Icon(Icons.refresh_outlined),
|
||||
title: Text(context.l10n.appSettings_clearPathOnMaxRetry),
|
||||
subtitle: Text(context.l10n.appSettings_clearPathOnMaxRetrySubtitle),
|
||||
subtitle: Text(
|
||||
context.l10n.appSettings_clearPathOnMaxRetrySubtitle,
|
||||
),
|
||||
value: settingsService.settings.clearPathOnMaxRetry,
|
||||
onChanged: (value) {
|
||||
settingsService.setClearPathOnMaxRetry(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared),
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_pathsWillBeCleared
|
||||
: context.l10n.appSettings_pathsWillNotBeCleared,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -241,9 +300,11 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
settingsService.setAutoRouteRotationEnabled(value);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled),
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_autoRouteRotationEnabled
|
||||
: context.l10n.appSettings_autoRouteRotationDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
@@ -254,7 +315,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMapSettingsCard(BuildContext context, AppSettingsService settingsService) {
|
||||
Widget _buildMapSettingsCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -302,12 +366,26 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
subtitle: Text(
|
||||
settingsService.settings.mapTimeFilterHours == 0
|
||||
? context.l10n.appSettings_timeFilterShowAll
|
||||
: context.l10n.appSettings_timeFilterShowLast(settingsService.settings.mapTimeFilterHours.toInt()),
|
||||
: context.l10n.appSettings_timeFilterShowLast(
|
||||
settingsService.settings.mapTimeFilterHours.toInt(),
|
||||
),
|
||||
),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _showTimeFilterDialog(context, settingsService),
|
||||
),
|
||||
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(
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(context.l10n.appSettings_offlineMapCache),
|
||||
@@ -332,6 +410,7 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// Fixed rendering issues
|
||||
Widget _buildBatteryCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
@@ -339,13 +418,15 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
) {
|
||||
final deviceId = connector.deviceId;
|
||||
final isConnected = connector.isConnected && deviceId != null;
|
||||
final selection =
|
||||
isConnected ? settingsService.batteryChemistryForDevice(deviceId) : 'nmc';
|
||||
final selection = isConnected
|
||||
? settingsService.batteryChemistryForDevice(deviceId)
|
||||
: 'nmc';
|
||||
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 4),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
child: Text(
|
||||
@@ -353,20 +434,38 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
),
|
||||
|
||||
// Main tile (icon + text only)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.battery_full),
|
||||
title: Text(context.l10n.appSettings_batteryChemistry),
|
||||
subtitle: Text(
|
||||
isConnected
|
||||
? context.l10n.appSettings_batteryChemistryPerDevice(connector.deviceDisplayName)
|
||||
? context.l10n.appSettings_batteryChemistryPerDevice(
|
||||
connector.deviceDisplayName,
|
||||
)
|
||||
: context.l10n.appSettings_batteryChemistryConnectFirst,
|
||||
),
|
||||
trailing: DropdownButton<String>(
|
||||
value: selection,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
),
|
||||
|
||||
// Dropdown (separate full-width row)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: DropdownButtonFormField<String>(
|
||||
initialValue: selection,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: isConnected
|
||||
? (value) {
|
||||
if (value != null) {
|
||||
settingsService.setBatteryChemistryForDevice(deviceId, value);
|
||||
settingsService.setBatteryChemistryForDevice(
|
||||
deviceId,
|
||||
value,
|
||||
);
|
||||
}
|
||||
}
|
||||
: null,
|
||||
@@ -391,7 +490,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showThemeModeDialog(BuildContext context, AppSettingsService settingsService) {
|
||||
void _showThemeModeDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -471,12 +573,19 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
return context.l10n.appSettings_languageSk;
|
||||
case 'bg':
|
||||
return context.l10n.appSettings_languageBg;
|
||||
case 'ru':
|
||||
return context.l10n.appSettings_languageRu;
|
||||
case 'uk':
|
||||
return context.l10n.appSettings_languageUk;
|
||||
default:
|
||||
return context.l10n.appSettings_languageSystem;
|
||||
}
|
||||
}
|
||||
|
||||
void _showLanguageDialog(BuildContext context, AppSettingsService settingsService) {
|
||||
void _showLanguageDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -547,6 +656,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
title: Text(context.l10n.appSettings_languageBg),
|
||||
value: 'bg',
|
||||
),
|
||||
RadioListTile<String?>(
|
||||
title: Text(context.l10n.appSettings_languageRu),
|
||||
value: 'ru',
|
||||
),
|
||||
RadioListTile<String?>(
|
||||
title: Text(context.l10n.appSettings_languageUk),
|
||||
value: 'uk',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -561,7 +678,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _showTimeFilterDialog(BuildContext context, AppSettingsService settingsService) {
|
||||
void _showTimeFilterDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
@@ -581,33 +701,23 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_allTime),
|
||||
leading: Radio<double>(
|
||||
value: 0,
|
||||
),
|
||||
leading: Radio<double>(value: 0),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_lastHour),
|
||||
leading: Radio<double>(
|
||||
value: 1,
|
||||
),
|
||||
leading: Radio<double>(value: 1),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_last6Hours),
|
||||
leading: Radio<double>(
|
||||
value: 6,
|
||||
),
|
||||
leading: Radio<double>(value: 6),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_last24Hours),
|
||||
leading: Radio<double>(
|
||||
value: 24,
|
||||
),
|
||||
leading: Radio<double>(value: 24),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_lastWeek),
|
||||
leading: Radio<double>(
|
||||
value: 168,
|
||||
),
|
||||
leading: Radio<double>(value: 168),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -622,7 +732,50 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDebugCard(BuildContext context, AppSettingsService settingsService) {
|
||||
void _showUnitsDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_unitsTitle),
|
||||
content: RadioGroup<UnitSystem>(
|
||||
groupValue: settingsService.settings.unitSystem,
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
settingsService.setUnitSystem(value);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_unitsMetric),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.metric),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(context.l10n.appSettings_unitsImperial),
|
||||
leading: const Radio<UnitSystem>(value: UnitSystem.imperial),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(context.l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDebugCard(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
return Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -644,9 +797,11 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled),
|
||||
content: Text(
|
||||
value
|
||||
? context.l10n.appSettings_appDebugLoggingEnabled
|
||||
: context.l10n.appSettings_appDebugLoggingDisabled,
|
||||
),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'package:flutter/services.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/ble_debug_log_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
@@ -24,10 +25,12 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final entries = logService.entries.reversed.toList();
|
||||
final rawEntries = logService.rawLogRxEntries.reversed.toList();
|
||||
final showingFrames = _view == _BleLogView.frames;
|
||||
final hasEntries = showingFrames ? entries.isNotEmpty : rawEntries.isNotEmpty;
|
||||
final hasEntries = showingFrames
|
||||
? entries.isNotEmpty
|
||||
: rawEntries.isNotEmpty;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.debugLog_bleTitle),
|
||||
title: AdaptiveAppBarTitle(context.l10n.debugLog_bleTitle),
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: context.l10n.debugLog_copyLog,
|
||||
@@ -36,15 +39,23 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
? () async {
|
||||
final text = showingFrames
|
||||
? entries
|
||||
.map((entry) => '${entry.description}\n${entry.hexPreview}\n')
|
||||
.join('\n')
|
||||
.map(
|
||||
(entry) =>
|
||||
'${entry.description}\n${entry.hexPreview}\n',
|
||||
)
|
||||
.join('\n')
|
||||
: rawEntries
|
||||
.map((entry) => 'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n')
|
||||
.join('\n');
|
||||
.map(
|
||||
(entry) =>
|
||||
'RX RAW_LOG_RX_DATA\n${entry.hexPreview}\n',
|
||||
)
|
||||
.join('\n');
|
||||
await Clipboard.setData(ClipboardData(text: text));
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.debugLog_bleCopied)),
|
||||
SnackBar(
|
||||
content: Text(context.l10n.debugLog_bleCopied),
|
||||
),
|
||||
);
|
||||
}
|
||||
: null,
|
||||
@@ -68,8 +79,14 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 0),
|
||||
child: SegmentedButton<_BleLogView>(
|
||||
segments: [
|
||||
ButtonSegment(value: _BleLogView.frames, label: Text(context.l10n.debugLog_frames)),
|
||||
ButtonSegment(value: _BleLogView.rawLogRx, label: Text(context.l10n.debugLog_rawLogRx)),
|
||||
ButtonSegment(
|
||||
value: _BleLogView.frames,
|
||||
label: Text(context.l10n.debugLog_frames),
|
||||
),
|
||||
ButtonSegment(
|
||||
value: _BleLogView.rawLogRx,
|
||||
label: Text(context.l10n.debugLog_rawLogRx),
|
||||
),
|
||||
],
|
||||
selected: {_view},
|
||||
onSelectionChanged: (selection) {
|
||||
@@ -81,8 +98,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
Expanded(
|
||||
child: hasEntries
|
||||
? ListView.separated(
|
||||
itemCount: showingFrames ? entries.length : rawEntries.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
itemCount: showingFrames
|
||||
? entries.length
|
||||
: rawEntries.length,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
if (showingFrames) {
|
||||
final entry = entries[index];
|
||||
@@ -94,7 +113,9 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
subtitle: Text('${entry.hexPreview}\n$time'),
|
||||
isThreeLine: true,
|
||||
leading: Icon(
|
||||
entry.outgoing ? Icons.upload : Icons.download,
|
||||
entry.outgoing
|
||||
? Icons.upload
|
||||
: Icons.download,
|
||||
size: 18,
|
||||
),
|
||||
);
|
||||
@@ -131,9 +152,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(info.title),
|
||||
content: SingleChildScrollView(
|
||||
child: SelectableText(info.rawHex),
|
||||
),
|
||||
content: SingleChildScrollView(child: SelectableText(info.rawHex)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
@@ -195,11 +214,18 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
}
|
||||
final payload = raw.sublist(index);
|
||||
|
||||
final title = 'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
||||
final title =
|
||||
'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
||||
final summary = _decodePayloadSummary(payloadType, payload);
|
||||
final pathSummary = pathLen > 0 ? 'Path=${_bytesToHex(pathBytes)}' : 'Path=none';
|
||||
final pathSummary = pathLen > 0
|
||||
? 'Path=${_bytesToHex(pathBytes)}'
|
||||
: 'Path=none';
|
||||
final detail = '$summary • $pathSummary • len=${raw.length}';
|
||||
return _RawPacketInfo(title: title, summary: detail, rawHex: _bytesToHex(raw));
|
||||
return _RawPacketInfo(
|
||||
title: title,
|
||||
summary: detail,
|
||||
rawHex: _bytesToHex(raw),
|
||||
);
|
||||
}
|
||||
|
||||
String _decodePayloadSummary(int payloadType, Uint8List payload) {
|
||||
@@ -245,7 +271,10 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
return 'ADVERT (short)';
|
||||
}
|
||||
var offset = 0;
|
||||
final pubKey = _bytesToHex(payload.sublist(offset, offset + 32), spaced: false);
|
||||
final pubKey = _bytesToHex(
|
||||
payload.sublist(offset, offset + 32),
|
||||
spaced: false,
|
||||
);
|
||||
offset += 32;
|
||||
final timestamp = readUint32LE(payload, offset);
|
||||
offset += 4;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,22 +4,27 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/channel_message.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final ChannelMessage message;
|
||||
|
||||
final bool channelMessage;
|
||||
const ChannelMessagePathScreen({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.channelMessage = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -27,7 +32,15 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final l10n = context.l10n;
|
||||
final primaryPath = _selectPrimaryPath(message.pathBytes, message.pathVariants);
|
||||
final primaryPathTmp = _selectPrimaryPath(
|
||||
message.pathBytes,
|
||||
message.pathVariants,
|
||||
);
|
||||
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
|
||||
final hops = _buildPathHops(primaryPath, connector.contacts, l10n);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
@@ -36,17 +49,31 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
l10n,
|
||||
);
|
||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.channelPath_title),
|
||||
title: AdaptiveAppBarTitle(l10n.channelPath_title),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.radar_outlined),
|
||||
tooltip: l10n.channelPath_viewMap,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: primaryPath,
|
||||
flipPathRound: true,
|
||||
reversePathRound: !message.isOutgoing && !channelMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.map_outlined),
|
||||
tooltip: l10n.channelPath_viewMap,
|
||||
onPressed: hasHopDetails
|
||||
? () {
|
||||
_openPathMap(context);
|
||||
_openPathMap(context, channelMessage: channelMessage);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
@@ -88,10 +115,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(
|
||||
BuildContext context, {
|
||||
String? observedLabel,
|
||||
}) {
|
||||
Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -105,21 +129,28 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_buildDetailRow(l10n.channelPath_senderLabel, message.senderName),
|
||||
_buildDetailRow(l10n.channelPath_timeLabel, _formatTime(message.timestamp, l10n)),
|
||||
_buildDetailRow(
|
||||
l10n.channelPath_timeLabel,
|
||||
_formatTime(message.timestamp, l10n),
|
||||
),
|
||||
if (message.repeatCount > 0)
|
||||
_buildDetailRow(l10n.channelPath_repeatsLabel, message.repeatCount.toString()),
|
||||
_buildDetailRow(l10n.channelPath_pathLabelTitle, _formatPathLabel(message.pathLength, l10n)),
|
||||
if (observedLabel != null) _buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
|
||||
_buildDetailRow(
|
||||
l10n.channelPath_repeatsLabel,
|
||||
message.repeatCount.toString(),
|
||||
),
|
||||
_buildDetailRow(
|
||||
l10n.channelPath_pathLabelTitle,
|
||||
_formatPathLabel(message.pathLength, l10n),
|
||||
),
|
||||
if (observedLabel != null)
|
||||
_buildDetailRow(l10n.channelPath_observedLabel, observedLabel),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPathVariants(
|
||||
BuildContext context,
|
||||
List<Uint8List> variants,
|
||||
) {
|
||||
Widget _buildPathVariants(BuildContext context, List<Uint8List> variants) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -137,7 +168,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
subtitle: Text(_formatPathPrefixes(variants[i])),
|
||||
trailing: const Icon(Icons.map_outlined, size: 20),
|
||||
onTap: () => _openPathMap(context, initialPath: variants[i]),
|
||||
onTap: () => _openPathMap(
|
||||
context,
|
||||
initialPath: variants[i],
|
||||
channelMessage: channelMessage,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -163,7 +198,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
subtitle: Text(
|
||||
hop.hasLocation
|
||||
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||
: l10n.channelPath_noLocationData,
|
||||
),
|
||||
),
|
||||
@@ -228,28 +263,34 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
void _openPathMap(BuildContext context, {Uint8List? initialPath}) {
|
||||
void _openPathMap(
|
||||
BuildContext context, {
|
||||
Uint8List? initialPath,
|
||||
bool channelMessage = false,
|
||||
}) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => ChannelMessagePathMapScreen(
|
||||
message: message,
|
||||
initialPath: initialPath,
|
||||
channelMessage: channelMessage,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
final ChannelMessage message;
|
||||
final Uint8List? initialPath;
|
||||
final bool channelMessage;
|
||||
|
||||
const ChannelMessagePathMapScreen({
|
||||
super.key,
|
||||
required this.message,
|
||||
this.initialPath,
|
||||
this.channelMessage = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -257,8 +298,14 @@ class ChannelMessagePathMapScreen extends StatefulWidget {
|
||||
_ChannelMessagePathMapScreenState();
|
||||
}
|
||||
|
||||
class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScreen> {
|
||||
class _ChannelMessagePathMapScreenState
|
||||
extends State<ChannelMessagePathMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
Uint8List? _selectedPath;
|
||||
double _pathDistance = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
bool _didReceivePositionUpdate = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -270,32 +317,77 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
void didUpdateWidget(ChannelMessagePathMapScreen oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.message != widget.message ||
|
||||
!_pathsEqual(oldWidget.initialPath ?? Uint8List(0),
|
||||
widget.initialPath ?? Uint8List(0))) {
|
||||
!_pathsEqual(
|
||||
oldWidget.initialPath ?? Uint8List(0),
|
||||
widget.initialPath ?? Uint8List(0),
|
||||
)) {
|
||||
_selectedPath = widget.initialPath;
|
||||
}
|
||||
}
|
||||
|
||||
double _getPathDistance(List<LatLng> points) {
|
||||
double totalDistance = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
|
||||
for (int i = 0; i < points.length - 1; i++) {
|
||||
totalDistance += distanceCalculator(points[i], points[i + 1]);
|
||||
}
|
||||
|
||||
return totalDistance;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final primaryPath =
|
||||
_selectPrimaryPath(widget.message.pathBytes, widget.message.pathVariants);
|
||||
final observedPaths =
|
||||
_buildObservedPaths(primaryPath, widget.message.pathVariants);
|
||||
final selectedPath = _resolveSelectedPath(
|
||||
final primaryPath = _selectPrimaryPath(
|
||||
widget.message.pathBytes,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final observedPaths = _buildObservedPaths(
|
||||
primaryPath,
|
||||
widget.message.pathVariants,
|
||||
);
|
||||
final selectedPathTmp = _resolveSelectedPath(
|
||||
_selectedPath,
|
||||
observedPaths,
|
||||
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 hops = _buildPathHops(selectedPath, connector.contacts, context.l10n);
|
||||
final points = hops
|
||||
.where((hop) => hop.hasLocation)
|
||||
.map((hop) => hop.position!)
|
||||
.toList();
|
||||
final hops = _buildPathHops(
|
||||
selectedPath,
|
||||
connector.contacts,
|
||||
context.l10n,
|
||||
);
|
||||
|
||||
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) {
|
||||
if (hop.hasLocation) {
|
||||
points.add(hop.position!);
|
||||
}
|
||||
}
|
||||
|
||||
if ((!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(!widget.message.isOutgoing && widget.channelMessage)) {
|
||||
points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
}
|
||||
|
||||
final polylines = points.length > 1
|
||||
? [
|
||||
Polyline(
|
||||
@@ -306,15 +398,24 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
]
|
||||
: <Polyline>[];
|
||||
|
||||
final initialCenter =
|
||||
points.isNotEmpty ? points.first : const LatLng(0, 0);
|
||||
final initialCenter = points.isNotEmpty
|
||||
? points.first
|
||||
: const LatLng(0, 0);
|
||||
final initialZoom = points.isNotEmpty ? 13.0 : 2.0;
|
||||
final bounds = points.length > 1 ? LatLngBounds.fromPoints(points) : null;
|
||||
final mapKey = ValueKey(_formatPathPrefixes(selectedPath));
|
||||
if (!_didReceivePositionUpdate) {
|
||||
_showNodeLabels = initialZoom >= _labelZoomThreshold;
|
||||
}
|
||||
final bounds = points.length > 1
|
||||
? LatLngBounds.fromPoints(points)
|
||||
: null;
|
||||
final mapKey = ValueKey(
|
||||
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}',
|
||||
);
|
||||
_pathDistance = _getPathDistance(points);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.channelPath_mapTitle),
|
||||
title: AdaptiveAppBarTitle(context.l10n.channelPath_mapTitle),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
@@ -334,6 +435,20 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
interactionOptions: InteractionOptions(
|
||||
flags: ~InteractiveFlag.rotate,
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (!_didReceivePositionUpdate ||
|
||||
shouldShow != _showNodeLabels) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_didReceivePositionUpdate = true;
|
||||
_showNodeLabels = shouldShow;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
@@ -343,34 +458,37 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
MapTileCacheService.userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
),
|
||||
if (polylines.isNotEmpty) PolylineLayer(polylines: polylines),
|
||||
if (polylines.isNotEmpty)
|
||||
PolylineLayer(polylines: polylines),
|
||||
MarkerLayer(
|
||||
markers: _buildHopMarkers(hops),
|
||||
markers: _buildHopMarkers(
|
||||
hops,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (observedPaths.length > 1)
|
||||
_buildPathSelector(
|
||||
context,
|
||||
observedPaths,
|
||||
selectedIndex,
|
||||
(index) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildPathSelector(context, observedPaths, selectedIndex, (
|
||||
index,
|
||||
) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
});
|
||||
}),
|
||||
if (points.isEmpty)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(context.l10n.channelPath_noRepeaterLocations),
|
||||
child: Text(
|
||||
context.l10n.channelPath_noRepeaterLocations,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildLegendCard(context, hops),
|
||||
_buildLegendCard(context, hops, isImperial),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -442,42 +560,141 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(List<_PathHop> hops) {
|
||||
return [
|
||||
for (final hop in hops)
|
||||
if (hop.hasLocation)
|
||||
Marker(
|
||||
point: hop.position!,
|
||||
width: 40,
|
||||
height: 40,
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<_PathHop> hops, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
for (final hop in hops) {
|
||||
if (!hop.hasLocation) continue;
|
||||
final point = hop.position!;
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
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,
|
||||
height: 35,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.teal,
|
||||
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(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: selfPoint,
|
||||
label: context.l10n.pathTrace_you,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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.green,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hop.index.toString(),
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(BuildContext context, List<_PathHop> hops) {
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
List<_PathHop> hops,
|
||||
bool isImperial,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||
final estimatedHeight = 72.0 + (hops.length * 56.0);
|
||||
@@ -496,7 +713,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
l10n.channelPath_repeaterHops,
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistance, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
@@ -509,7 +726,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: hops.length,
|
||||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final hop = hops[index];
|
||||
return ListTile(
|
||||
@@ -525,7 +742,7 @@ class _ChannelMessagePathMapScreenState extends State<ChannelMessagePathMapScree
|
||||
subtitle: Text(
|
||||
hop.hasLocation
|
||||
? '${hop.position!.latitude.toStringAsFixed(5)}, '
|
||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||
'${hop.position!.longitude.toStringAsFixed(5)}'
|
||||
: l10n.channelPath_noLocationData,
|
||||
),
|
||||
);
|
||||
@@ -567,10 +784,7 @@ class _ObservedPath {
|
||||
final Uint8List pathBytes;
|
||||
final bool isPrimary;
|
||||
|
||||
const _ObservedPath({
|
||||
required this.pathBytes,
|
||||
required this.isPrimary,
|
||||
});
|
||||
const _ObservedPath({required this.pathBytes, required this.isPrimary});
|
||||
}
|
||||
|
||||
List<_PathHop> _buildPathHops(
|
||||
@@ -597,10 +811,12 @@ List<_PathHop> _buildPathHops(
|
||||
|
||||
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)
|
||||
.where(
|
||||
(contact) =>
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
contact.publicKey.isNotEmpty &&
|
||||
contact.publicKey[0] == prefix,
|
||||
)
|
||||
.toList();
|
||||
if (matches.isEmpty) return null;
|
||||
|
||||
|
||||
+562
-413
File diff suppressed because it is too large
Load Diff
+835
-419
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 '../models/community.dart';
|
||||
import '../storage/community_store.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/qr_scanner_widget.dart';
|
||||
|
||||
/// Screen for scanning community QR codes to join communities.
|
||||
@@ -29,7 +30,7 @@ class _CommunityQrScannerScreenState extends State<CommunityQrScannerScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.community_scanQr),
|
||||
title: AdaptiveAppBarTitle(context.l10n.community_scanQr),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: _isProcessing
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
@@ -14,7 +18,6 @@ import '../utils/dialog_utils.dart';
|
||||
import '../utils/disconnect_navigation_mixin.dart';
|
||||
import '../utils/emoji_utils.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/battery_indicator.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
@@ -23,14 +26,14 @@ import '../widgets/room_login_dialog.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
import 'discovery_screen.dart';
|
||||
import 'map_screen.dart';
|
||||
import 'repeater_hub_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
|
||||
enum RoomLoginDestination {
|
||||
chat,
|
||||
management,
|
||||
}
|
||||
enum RoomLoginDestination { chat, management }
|
||||
|
||||
enum ContactOperationType { import, export, zeroHopShare }
|
||||
|
||||
class ContactsScreen extends StatefulWidget {
|
||||
final bool hideBackButton;
|
||||
@@ -52,16 +55,22 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
List<ContactGroup> _groups = [];
|
||||
Timer? _searchDebounce;
|
||||
|
||||
final Set<ContactOperationType> _pendingOperations = {};
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadGroups();
|
||||
_setupFrameListener();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchDebounce?.cancel();
|
||||
_searchController.dispose();
|
||||
_frameSubscription?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -77,6 +86,152 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
await _groupStore.saveGroups(_groups);
|
||||
}
|
||||
|
||||
void _setupFrameListener() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final frameBuffer = BufferReader(frame);
|
||||
try {
|
||||
final code = frameBuffer.readUInt8();
|
||||
|
||||
if (code == respCodeExportContact) {
|
||||
final advertPacket = frameBuffer.readRemainingBytes();
|
||||
// Validate packet has expected minimum size (98+ bytes per protocol)
|
||||
if (advertPacket.length < 98) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_invalidAdvertFormat),
|
||||
),
|
||||
);
|
||||
}
|
||||
_pendingOperations.remove(ContactOperationType.export);
|
||||
return;
|
||||
}
|
||||
final hexString = pubKeyToHex(advertPacket);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
}
|
||||
|
||||
if (code == respCodeOk) {
|
||||
// Show a snackbar indicating success
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactImported)),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertSent),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopied),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
|
||||
if (code == respCodeErr) {
|
||||
// Show a snackbar indicating failure
|
||||
if (!mounted) return;
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.import)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactImportFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_pendingOperations.contains(ContactOperationType.zeroHopShare)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_zeroHopContactAdvertFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
if (_pendingOperations.contains(ContactOperationType.export)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.contacts_contactAdvertCopyFailed),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_pendingOperations.clear();
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error processing received frame: $e',
|
||||
tag: 'ContactsScreen',
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _contactExport(Uint8List pubKey) async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactFrame = buildExportContactFrame(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.export);
|
||||
await connector.sendFrame(exportContactFrame, expectsGenericAck: true);
|
||||
}
|
||||
|
||||
Future<void> _contactZeroHop(Uint8List pubKey) async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final exportContactZeroHopFrame = buildZeroHopContact(pubKey);
|
||||
_pendingOperations.add(ContactOperationType.zeroHopShare);
|
||||
await connector.sendFrame(
|
||||
exportContactZeroHopFrame,
|
||||
expectsGenericAck: true,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _contactImport() async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final clipboardData = await Clipboard.getData('text/plain');
|
||||
if (clipboardData == null || clipboardData.text == null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_clipboardEmpty)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final text = clipboardData.text!.trim();
|
||||
if (!text.startsWith('meshcore://')) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
final hexString = text.substring('meshcore://'.length);
|
||||
try {
|
||||
final bytes = hex2Uint8List(hexString);
|
||||
final importContactFrame = buildImportContactFrame(bytes);
|
||||
_pendingOperations.add(ContactOperationType.import);
|
||||
connector.importContact(importContactFrame);
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_invalidAdvertFormat)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
@@ -91,23 +246,112 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
leading: BatteryIndicator(connector: connector),
|
||||
title: Text(context.l10n.contacts_title),
|
||||
centerTitle: true,
|
||||
title: AppBarTitle(context.l10n.contacts_title),
|
||||
automaticallyImplyLeading: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bluetooth_disabled),
|
||||
tooltip: context.l10n.common_disconnect,
|
||||
onPressed: () => _disconnect(context, connector),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.connect_without_contact),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_zeroHopAdvert),
|
||||
],
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: false),
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.cell_tower),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_floodAdvert),
|
||||
],
|
||||
),
|
||||
onTap: () => {
|
||||
connector.sendSelfAdvert(flood: true),
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.settings_advertisementSent),
|
||||
),
|
||||
),
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.copy),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_copyAdvertToClipboard),
|
||||
],
|
||||
),
|
||||
onTap: () => _contactExport(Uint8List.fromList([])),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.paste),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.contacts_addContactFromClipboard),
|
||||
],
|
||||
),
|
||||
onTap: () => _contactImport(),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.connect_without_contact),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.tune),
|
||||
tooltip: context.l10n.common_settings,
|
||||
onPressed: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsScreen()),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.logout, color: Colors.red),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.common_disconnect),
|
||||
],
|
||||
),
|
||||
onTap: () => _disconnect(context, connector),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_add_rounded),
|
||||
const SizedBox(width: 8),
|
||||
Text("Discovered Contacts"),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.settings_title),
|
||||
],
|
||||
),
|
||||
onTap: () => Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsScreen(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -157,8 +401,15 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
|
||||
Widget _buildContactsBody(BuildContext context, MeshCoreConnector connector) {
|
||||
final contacts = connector.contacts;
|
||||
final shouldShowStartupSpinner =
|
||||
contacts.isEmpty &&
|
||||
_groups.isEmpty &&
|
||||
connector.isConnected &&
|
||||
(connector.isLoadingContacts ||
|
||||
connector.isLoadingChannels ||
|
||||
connector.selfPublicKey == null);
|
||||
|
||||
if (contacts.isEmpty && connector.isLoadingContacts && _groups.isEmpty) {
|
||||
if (shouldShowStartupSpinner) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
@@ -175,6 +426,41 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
? const <ContactGroup>[]
|
||||
: _filterAndSortGroups(_groups, contacts);
|
||||
|
||||
String hintText = "";
|
||||
|
||||
switch (_typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
hintText = context.l10n.contacts_searchContacts(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.users:
|
||||
hintText = context.l10n.contacts_searchUsers(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.repeaters:
|
||||
hintText = context.l10n.contacts_searchRepeaters(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.rooms:
|
||||
hintText = context.l10n.contacts_searchRoomServers(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.favorites:
|
||||
hintText = context.l10n.contacts_searchFavorites(
|
||||
filteredAndSorted.length,
|
||||
_showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
@@ -182,7 +468,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.contacts_searchContacts,
|
||||
hintText: hintText,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -254,6 +540,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
contact: contact,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: contact.isFavorite,
|
||||
onTap: () => _openChat(context, contact),
|
||||
onLongPress: () =>
|
||||
_showContactOptions(context, connector, contact),
|
||||
@@ -290,6 +577,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
})
|
||||
.where((group) {
|
||||
if (_typeFilter == ContactTypeFilter.all) return true;
|
||||
// Groups don't have a favorite flag, so hide them under favorites filter
|
||||
if (_typeFilter == ContactTypeFilter.favorites) return false;
|
||||
for (final key in group.memberKeys) {
|
||||
final contact = contactsByKey[key];
|
||||
if (contact != null && _matchesTypeFilter(contact)) return true;
|
||||
@@ -313,6 +602,14 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return matchesContactQuery(contact, _searchQuery);
|
||||
}).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();
|
||||
}
|
||||
@@ -356,6 +653,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
switch (_typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
return true;
|
||||
case ContactTypeFilter.favorites:
|
||||
return contact.isFavorite;
|
||||
case ContactTypeFilter.users:
|
||||
return contact.type == advTypeChat;
|
||||
case ContactTypeFilter.repeaters:
|
||||
@@ -489,7 +788,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => destination == RoomLoginDestination.management
|
||||
builder: (context) =>
|
||||
destination == RoomLoginDestination.management
|
||||
? RepeaterHubScreen(repeater: room, password: password)
|
||||
: ChatScreen(contact: room),
|
||||
),
|
||||
@@ -745,6 +1045,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
) {
|
||||
final isRepeater = contact.type == advTypeRepeater;
|
||||
final isRoom = contact.type == advTypeRoom;
|
||||
final isFavorite = contact.isFavorite;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -752,7 +1053,26 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isRepeater)
|
||||
if (isRepeater) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: contact.pathLength > 0
|
||||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: contact.pathLength > 0
|
||||
? context.l10n.contacts_repeaterPathTrace
|
||||
: context.l10n.contacts_repeaterPing,
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.cell_tower, color: Colors.orange),
|
||||
title: Text(context.l10n.contacts_manageRepeater),
|
||||
@@ -760,8 +1080,27 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
Navigator.pop(sheetContext);
|
||||
_showRepeaterLogin(context, contact);
|
||||
},
|
||||
)
|
||||
else if (isRoom) ...[
|
||||
),
|
||||
] else if (isRoom) ...[
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: contact.pathLength > 0
|
||||
? Text(context.l10n.contacts_pathTrace)
|
||||
: Text(context.l10n.contacts_ping),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: contact.pathLength > 0
|
||||
? context.l10n.contacts_roomPathTrace
|
||||
: context.l10n.contacts_roomPing,
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.room, color: Colors.blue),
|
||||
title: Text(context.l10n.contacts_roomLogin),
|
||||
@@ -771,14 +1110,40 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.room_preferences, color: Colors.orange),
|
||||
leading: const Icon(
|
||||
Icons.room_preferences,
|
||||
color: Colors.orange,
|
||||
),
|
||||
title: Text(context.l10n.room_management),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_showRoomLogin(context, contact, RoomLoginDestination.management);
|
||||
_showRoomLogin(
|
||||
context,
|
||||
contact,
|
||||
RoomLoginDestination.management,
|
||||
);
|
||||
},
|
||||
),
|
||||
] else
|
||||
] else ...[
|
||||
if (contact.pathLength > 0)
|
||||
ListTile(
|
||||
leading: const Icon(Icons.radar, color: Colors.green),
|
||||
title: Text(context.l10n.contacts_chatTraceRoute),
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_pathTraceTo(
|
||||
contact.name,
|
||||
),
|
||||
path: contact.traceRouteBytes ?? Uint8List(0),
|
||||
targetContact: contact,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.chat),
|
||||
title: Text(context.l10n.contacts_openChat),
|
||||
@@ -787,6 +1152,38 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
_openChat(context, contact);
|
||||
},
|
||||
),
|
||||
],
|
||||
ListTile(
|
||||
leading: Icon(
|
||||
isFavorite ? Icons.star : Icons.star_border,
|
||||
color: Colors.amber[700],
|
||||
),
|
||||
title: Text(
|
||||
isFavorite
|
||||
? context.l10n.listFilter_removeFromFavorites
|
||||
: context.l10n.listFilter_addToFavorites,
|
||||
),
|
||||
onTap: () async {
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setContactFavorite(contact, !isFavorite);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(context.l10n.contacts_ShareContact),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_contactExport(contact.publicKey);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.connect_without_contact),
|
||||
title: Text(context.l10n.contacts_ShareContactZeroHop),
|
||||
onTap: () {
|
||||
Navigator.pop(sheetContext);
|
||||
_contactZeroHop(contact.publicKey);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete, color: Colors.red),
|
||||
title: Text(
|
||||
@@ -839,6 +1236,7 @@ class _ContactTile extends StatelessWidget {
|
||||
final Contact contact;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
final VoidCallback onTap;
|
||||
final VoidCallback onLongPress;
|
||||
|
||||
@@ -846,39 +1244,71 @@ class _ContactTile extends StatelessWidget {
|
||||
required this.contact,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
required this.onTap,
|
||||
required this.onLongPress,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shotPublicKey =
|
||||
"<${contact.publicKeyHex.substring(0, 8)}...${contact.publicKeyHex.substring(contact.publicKeyHex.length - 8)}>";
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
child: _buildContactAvatar(contact),
|
||||
),
|
||||
title: Text(contact.name),
|
||||
subtitle: Text(
|
||||
'${contact.typeLabel} • ${contact.pathLabel} $shotPublicKey',
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
title: Text(contact.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Text(contact.pathLabel, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
Text(
|
||||
_formatLastSeen(context, lastSeen),
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
contact.shortPubKeyHex,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||
],
|
||||
),
|
||||
// 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: [
|
||||
if (unreadCount > 0) ...[
|
||||
UnreadBadge(count: unreadCount),
|
||||
const SizedBox(height: 4),
|
||||
],
|
||||
Text(
|
||||
_formatLastSeen(context, lastSeen),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.right,
|
||||
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||
),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isFavorite)
|
||||
Icon(Icons.star, size: 14, color: Colors.amber[700]),
|
||||
if (isFavorite && contact.hasLocation)
|
||||
const SizedBox(width: 2),
|
||||
if (contact.hasLocation)
|
||||
Icon(Icons.location_on, size: 14, color: Colors.grey[400]),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: onTap,
|
||||
onLongPress: onLongPress,
|
||||
);
|
||||
|
||||
@@ -127,9 +127,7 @@ class _DeviceScreenState extends State<DeviceScreen>
|
||||
return Card(
|
||||
elevation: 0,
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
@@ -207,7 +205,6 @@ class _DeviceScreenState extends State<DeviceScreen>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Widget _buildBatteryIndicator(
|
||||
MeshCoreConnector connector,
|
||||
BuildContext context,
|
||||
@@ -224,11 +221,7 @@ class _DeviceScreenState extends State<DeviceScreen>
|
||||
final icon = _batteryIcon(percent);
|
||||
|
||||
return ActionChip(
|
||||
avatar: Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
avatar: Icon(icon, size: 16, color: colorScheme.onSecondaryContainer),
|
||||
label: Text(displayLabel),
|
||||
labelStyle: theme.textTheme.labelMedium?.copyWith(
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
@@ -260,25 +253,19 @@ class _DeviceScreenState extends State<DeviceScreen>
|
||||
case 0:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(
|
||||
const ContactsScreen(hideBackButton: true),
|
||||
),
|
||||
buildQuickSwitchRoute(const ContactsScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
case 1:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(
|
||||
const ChannelsScreen(hideBackButton: true),
|
||||
),
|
||||
buildQuickSwitchRoute(const ChannelsScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
case 2:
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
buildQuickSwitchRoute(
|
||||
const MapScreen(hideBackButton: true),
|
||||
),
|
||||
buildQuickSwitchRoute(const MapScreen(hideBackButton: true)),
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/discovery_contact.dart';
|
||||
import '../utils/contact_search.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
import '../widgets/list_filter_widget.dart';
|
||||
|
||||
enum DiscoverySortOption { lastSeen, name, type }
|
||||
|
||||
class DiscoveryScreen extends StatefulWidget {
|
||||
const DiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
String searchQuery = '';
|
||||
ContactSortOption sortOption = ContactSortOption.lastSeen;
|
||||
bool showUnreadOnly = false;
|
||||
ContactTypeFilter typeFilter = ContactTypeFilter.all;
|
||||
DiscoverySortOption discoverySortOption = DiscoverySortOption.lastSeen;
|
||||
Timer? _searchDebounce;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchDebounce?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
|
||||
final discoveredContacts = connector.discoveredContacts;
|
||||
final filteredAndSorted = _filterAndSortContacts(
|
||||
discoveredContacts,
|
||||
connector,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(
|
||||
l10n.discoveredContacts_Title,
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.delete, color: Colors.red),
|
||||
const SizedBox(width: 8),
|
||||
Text(context.l10n.discoveredContacts_deleteContactAll),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
_deleteContacts(context, connector);
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: const Icon(Icons.more_vert),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
_buildFilters(filteredAndSorted, connector),
|
||||
Expanded(
|
||||
child: discoveredContacts.isEmpty
|
||||
? Center(child: Text(l10n.contacts_noContacts))
|
||||
: filteredAndSorted.isEmpty
|
||||
? Center(child: Text(l10n.discoveredContacts_noMatching))
|
||||
: ListView.builder(
|
||||
itemCount: filteredAndSorted.length,
|
||||
itemBuilder: (context, index) {
|
||||
final contact = filteredAndSorted[index];
|
||||
return ListTile(
|
||||
leading: CircleAvatar(
|
||||
backgroundColor: _getTypeColor(contact.type),
|
||||
child: Icon(
|
||||
_getTypeIcon(contact.type),
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
title: Text(
|
||||
contact.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
contact.shortPubKeyHex,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
trailing: Text(
|
||||
_formatLastSeen(context, contact.lastSeen),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
connector.importDiscoveredContact(contact);
|
||||
},
|
||||
onLongPress: () =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showContactContextMenu(
|
||||
DiscoveryContact contact,
|
||||
MeshCoreConnector connector,
|
||||
) async {
|
||||
final action = await showModalBottomSheet<String>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (sheetContext) {
|
||||
final l10n = context.l10n;
|
||||
return SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_reaction_sharp),
|
||||
title: Text(l10n.discoveredContacts_addContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('import_contact'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.copy),
|
||||
title: Text(l10n.discoveredContacts_copyContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('copy_contact'),
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.delete),
|
||||
title: Text(l10n.discoveredContacts_deleteContact),
|
||||
onTap: () => Navigator.of(sheetContext).pop('delete_contact'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (!mounted || action == null) return;
|
||||
|
||||
switch (action) {
|
||||
case 'import_contact':
|
||||
connector.importDiscoveredContact(contact);
|
||||
break;
|
||||
case 'copy_contact':
|
||||
final hexString = pubKeyToHex(contact.rawPacket);
|
||||
Clipboard.setData(ClipboardData(text: "meshcore://$hexString"));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.contacts_contactAdvertCopied)),
|
||||
);
|
||||
break;
|
||||
case 'delete_contact':
|
||||
connector.removeDiscoveredContact(contact);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteContacts(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.common_deleteAll),
|
||||
content: Text(l10n.discoveredContacts_deleteContactAllContent),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(context);
|
||||
connector.removeAllDiscoveredContacts();
|
||||
},
|
||||
child: Text(l10n.common_deleteAll),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilters(
|
||||
List<DiscoveryContact> filteredAndSorted,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
String hintText = "";
|
||||
switch (typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
hintText = context.l10n.contacts_searchContacts(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.users:
|
||||
hintText = context.l10n.contacts_searchUsers(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.repeaters:
|
||||
hintText = context.l10n.contacts_searchRepeaters(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.rooms:
|
||||
hintText = context.l10n.contacts_searchRoomServers(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
case ContactTypeFilter.favorites:
|
||||
hintText = context.l10n.contacts_searchFavorites(
|
||||
filteredAndSorted.length,
|
||||
showUnreadOnly ? " ${context.l10n.contacts_unread}" : "",
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: TextField(
|
||||
controller: _searchController,
|
||||
decoration: InputDecoration(
|
||||
hintText: hintText,
|
||||
prefixIcon: const Icon(Icons.search),
|
||||
suffixIcon: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (searchQuery.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear),
|
||||
onPressed: () {
|
||||
_searchController.clear();
|
||||
setState(() {
|
||||
searchQuery = '';
|
||||
});
|
||||
},
|
||||
),
|
||||
_buildFilterButton(context, connector),
|
||||
],
|
||||
),
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
onChanged: (value) {
|
||||
_searchDebounce?.cancel();
|
||||
_searchDebounce = Timer(const Duration(milliseconds: 300), () {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
searchQuery = value.toLowerCase();
|
||||
});
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFilterButton(BuildContext context, MeshCoreConnector connector) {
|
||||
return DiscoveryContactsFilterMenu(
|
||||
sortOption: sortOption,
|
||||
typeFilter: typeFilter,
|
||||
onSortChanged: (value) {
|
||||
setState(() {
|
||||
sortOption = value;
|
||||
});
|
||||
},
|
||||
onTypeFilterChanged: (value) {
|
||||
setState(() {
|
||||
typeFilter = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<DiscoveryContact> _filterAndSortContacts(
|
||||
List<DiscoveryContact> contacts,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
var filtered = contacts.where((contact) {
|
||||
if (searchQuery.isEmpty) return true;
|
||||
return matchesDiscoveryContactQuery(contact, searchQuery);
|
||||
}).toList();
|
||||
|
||||
filtered = filtered.where((contact) {
|
||||
return !connector.knownContactKeys.contains(contact.publicKeyHex);
|
||||
}).toList();
|
||||
|
||||
// Filter out own node from the list
|
||||
if (connector.selfPublicKey != null) {
|
||||
final selfPubKeyHex = pubKeyToHex(connector.selfPublicKey!);
|
||||
filtered = filtered.where((contact) {
|
||||
return contact.publicKeyHex != selfPubKeyHex;
|
||||
}).toList();
|
||||
}
|
||||
|
||||
if (typeFilter != ContactTypeFilter.all) {
|
||||
filtered = filtered.where(_matchesTypeFilter).toList();
|
||||
}
|
||||
|
||||
switch (sortOption) {
|
||||
case ContactSortOption.lastSeen:
|
||||
filtered.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
break;
|
||||
case ContactSortOption.name:
|
||||
filtered.sort(
|
||||
(a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return filtered;
|
||||
}
|
||||
|
||||
bool _matchesTypeFilter(DiscoveryContact contact) {
|
||||
switch (typeFilter) {
|
||||
case ContactTypeFilter.all:
|
||||
return true;
|
||||
case ContactTypeFilter.users:
|
||||
return contact.type == advTypeChat;
|
||||
case ContactTypeFilter.repeaters:
|
||||
return contact.type == advTypeRepeater;
|
||||
case ContactTypeFilter.rooms:
|
||||
return contact.type == advTypeRoom;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
IconData _getTypeIcon(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Icons.chat;
|
||||
case advTypeRepeater:
|
||||
return Icons.cell_tower;
|
||||
case advTypeRoom:
|
||||
return Icons.group;
|
||||
case advTypeSensor:
|
||||
return Icons.sensors;
|
||||
default:
|
||||
return Icons.device_unknown;
|
||||
}
|
||||
}
|
||||
|
||||
Color _getTypeColor(int type) {
|
||||
switch (type) {
|
||||
case advTypeChat:
|
||||
return Colors.blue;
|
||||
case advTypeRepeater:
|
||||
return Colors.orange;
|
||||
case advTypeRoom:
|
||||
return Colors.purple;
|
||||
case advTypeSensor:
|
||||
return Colors.green;
|
||||
default:
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatLastSeen(BuildContext context, DateTime lastSeen) {
|
||||
final now = DateTime.now();
|
||||
final diff = now.difference(lastSeen);
|
||||
|
||||
if (diff.isNegative || diff.inMinutes < 5) {
|
||||
return context.l10n.contacts_lastSeenNow;
|
||||
}
|
||||
if (diff.inMinutes < 60) {
|
||||
return context.l10n.contacts_lastSeenMinsAgo(diff.inMinutes);
|
||||
}
|
||||
if (diff.inHours < 24) {
|
||||
final hours = diff.inHours;
|
||||
return hours == 1
|
||||
? context.l10n.contacts_lastSeenHourAgo
|
||||
: context.l10n.contacts_lastSeenHoursAgo(hours);
|
||||
}
|
||||
final days = diff.inDays;
|
||||
return days == 1
|
||||
? context.l10n.contacts_lastSeenDayAgo
|
||||
: context.l10n.contacts_lastSeenDaysAgo(days);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,6 +7,7 @@ import '../l10n/app_localizations.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -56,10 +57,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
_updateEstimate();
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(48),
|
||||
),
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -72,8 +70,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
return;
|
||||
}
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
final count =
|
||||
cacheService.estimateTileCount(_selectedBounds!, _minZoom, _maxZoom);
|
||||
final count = cacheService.estimateTileCount(
|
||||
_selectedBounds!,
|
||||
_minZoom,
|
||||
_maxZoom,
|
||||
);
|
||||
setState(() {
|
||||
_estimatedTiles = count;
|
||||
});
|
||||
@@ -181,9 +182,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
result.failed,
|
||||
)
|
||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(message)));
|
||||
}
|
||||
|
||||
Future<void> _clearCache() async {
|
||||
@@ -225,7 +226,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.mapCache_title),
|
||||
title: AdaptiveAppBarTitle(l10n.mapCache_title),
|
||||
centerTitle: true,
|
||||
),
|
||||
body: Column(
|
||||
@@ -290,7 +291,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
children: [
|
||||
Text(
|
||||
l10n.mapCache_cacheArea,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
@@ -304,8 +308,9 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
TextButton(
|
||||
onPressed:
|
||||
_isDownloading || selectedBounds == null ? null : _clearBounds,
|
||||
onPressed: _isDownloading || selectedBounds == null
|
||||
? null
|
||||
: _clearBounds,
|
||||
child: Text(l10n.common_clear),
|
||||
),
|
||||
],
|
||||
@@ -313,11 +318,16 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.mapCache_zoomRange,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
RangeSlider(
|
||||
values:
|
||||
RangeValues(_minZoom.toDouble(), _maxZoom.toDouble()),
|
||||
values: RangeValues(
|
||||
_minZoom.toDouble(),
|
||||
_maxZoom.toDouble(),
|
||||
),
|
||||
min: 3,
|
||||
max: 18,
|
||||
divisions: 15,
|
||||
@@ -341,10 +351,12 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(value: progressValue),
|
||||
const SizedBox(height: 4),
|
||||
Text(l10n.mapCache_downloadedTiles(
|
||||
_completedTiles,
|
||||
_estimatedTiles,
|
||||
)),
|
||||
Text(
|
||||
l10n.mapCache_downloadedTiles(
|
||||
_completedTiles,
|
||||
_estimatedTiles,
|
||||
),
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
|
||||
+825
-191
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -11,28 +12,28 @@ import '../services/repeater_command_service.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
import '../widgets/snr_indicator.dart';
|
||||
|
||||
class NeighboursScreen extends StatefulWidget {
|
||||
class NeighborsScreen extends StatefulWidget {
|
||||
final Contact repeater;
|
||||
final String password;
|
||||
|
||||
const NeighboursScreen({
|
||||
const NeighborsScreen({
|
||||
super.key,
|
||||
required this.repeater,
|
||||
required this.password,
|
||||
});
|
||||
|
||||
@override
|
||||
State<NeighboursScreen> createState() => _NeighboursScreenState();
|
||||
State<NeighborsScreen> createState() => _NeighborsScreenState();
|
||||
}
|
||||
|
||||
class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
static const int _reqNeighboursKeyLen = 4;
|
||||
class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
static const int _reqNeighborsKeyLen = 4;
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
Uint8List _tagData = Uint8List(4);
|
||||
int _neighbourCount = 0;
|
||||
int _neighborCount = 0;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _isLoaded = false;
|
||||
@@ -41,7 +42,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
RepeaterCommandService? _commandService;
|
||||
PathSelection? _pendingStatusSelection;
|
||||
List<Map<String, dynamic>>? _parsedNeighbours;
|
||||
List<Map<String, dynamic>>? _parsedNeighbors;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -49,7 +50,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
_commandService = RepeaterCommandService(connector);
|
||||
_setupMessageListener();
|
||||
_loadNeighbours();
|
||||
_loadNeighbors();
|
||||
_hasData = false;
|
||||
}
|
||||
|
||||
@@ -62,13 +63,12 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
|
||||
if (frame[0] == respCodeSent) {
|
||||
_tagData = frame.sublist(2, 6);
|
||||
//_timeEstment = frame.buffer.asByteData().getUint32(6, Endian.little);
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame[0] == pushCodeBinaryResponse &&
|
||||
listEquals(frame.sublist(2, 6), _tagData)) {
|
||||
_handleNeighboursResponse(connector, frame.sublist(6));
|
||||
_handleNeighborsResponse(connector, frame.sublist(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -91,65 +91,77 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
return '${h}h ${m2}m';
|
||||
}
|
||||
|
||||
static List<Map<String, dynamic>> parseNeighboursData(
|
||||
static List<Map<String, dynamic>> parseNeighborsData(
|
||||
BufferReader buffer,
|
||||
int resultsCount,
|
||||
) {
|
||||
final Map<int, Map<String, dynamic>> neighbours = {};
|
||||
for (var i = 0; i < resultsCount; i++) {
|
||||
final neighbourData = neighbours.putIfAbsent(
|
||||
i,
|
||||
() => {
|
||||
'contact': null,
|
||||
'publicKey': <Uint8List>{},
|
||||
'lastHeard': <int>{},
|
||||
'snr': <double>{},
|
||||
},
|
||||
);
|
||||
neighbourData['publicKey'] = buffer.readBytes(_reqNeighboursKeyLen);
|
||||
neighbourData['lastHeard'] = buffer.readUInt32LE();
|
||||
neighbourData['snr'] = buffer.readInt8() / 4.0;
|
||||
}
|
||||
final Map<int, Map<String, dynamic>> neighbors = {};
|
||||
try {
|
||||
for (var i = 0; i < resultsCount; i++) {
|
||||
final neighborData = neighbors.putIfAbsent(
|
||||
i,
|
||||
() => {
|
||||
'contact': null,
|
||||
'publicKey': <Uint8List>{},
|
||||
'lastHeard': <int>{},
|
||||
'snr': <double>{},
|
||||
},
|
||||
);
|
||||
neighborData['publicKey'] = buffer.readBytes(_reqNeighborsKeyLen);
|
||||
neighborData['lastHeard'] = buffer.readUInt32LE();
|
||||
neighborData['snr'] = buffer.readInt8() / 4.0;
|
||||
}
|
||||
|
||||
return neighbours.values.toList();
|
||||
return neighbors.values.toList();
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error parsing neighbors data: $e',
|
||||
tag: 'NeighborsScreen',
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
void _handleNeighboursResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
void _handleNeighborsResponse(MeshCoreConnector connector, Uint8List frame) {
|
||||
final buffer = BufferReader(frame);
|
||||
final 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;
|
||||
try {
|
||||
final neighborCount = buffer.readUInt16LE();
|
||||
final parsedNeighbors = parseNeighborsData(buffer, buffer.readUInt16LE());
|
||||
connector.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(() {
|
||||
_parsedNeighbours = parsedNeighbours;
|
||||
_neighbourCount = neighbourCount;
|
||||
});
|
||||
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;
|
||||
});
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
Contact _resolveRepeater(MeshCoreConnector connector) {
|
||||
@@ -159,7 +171,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _loadNeighbours() async {
|
||||
Future<void> _loadNeighbors() async {
|
||||
if (_commandService == null) return;
|
||||
|
||||
setState(() {
|
||||
@@ -172,17 +184,17 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
final selection = await connector.preparePathForContactSend(repeater);
|
||||
_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(
|
||||
repeater.publicKey,
|
||||
payload: Uint8List.fromList([
|
||||
reqTypeGetNeighbours,
|
||||
reqTypeGetNeighbors,
|
||||
0x00,
|
||||
0x0F,
|
||||
0x00,
|
||||
0x00,
|
||||
0x00,
|
||||
_reqNeighboursKeyLen,
|
||||
_reqNeighborsKeyLen,
|
||||
]),
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
@@ -258,7 +270,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
l10n.neighbors_repeatersNeighbours,
|
||||
l10n.neighbors_repeatersNeighbors,
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
@@ -345,7 +357,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isLoading ? null : _loadNeighbours,
|
||||
onPressed: _isLoading ? null : _loadNeighbors,
|
||||
tooltip: l10n.repeater_refresh,
|
||||
),
|
||||
],
|
||||
@@ -353,13 +365,13 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: RefreshIndicator(
|
||||
onRefresh: _loadNeighbours,
|
||||
onRefresh: _loadNeighbors,
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
if (!_isLoaded &&
|
||||
!_hasData &&
|
||||
(_parsedNeighbours == null || _parsedNeighbours!.isEmpty))
|
||||
(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||
Center(
|
||||
child: Text(
|
||||
l10n.neighbors_noData,
|
||||
@@ -368,10 +380,9 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
),
|
||||
if (_isLoaded ||
|
||||
_hasData &&
|
||||
!(_parsedNeighbours == null ||
|
||||
_parsedNeighbours!.isEmpty))
|
||||
_buildNeighboursInfoCard(
|
||||
"${l10n.repeater_neighbours} - $_neighbourCount",
|
||||
!(_parsedNeighbors == null || _parsedNeighbors!.isEmpty))
|
||||
_buildNeighborsInfoCard(
|
||||
"${l10n.repeater_neighbors} - $_neighborCount",
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -380,7 +391,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNeighboursInfoCard(String title) {
|
||||
Widget _buildNeighborsInfoCard(String title) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
return Card(
|
||||
child: Padding(
|
||||
@@ -405,7 +416,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
for (final entry in _parsedNeighbours!.asMap().entries)
|
||||
for (final entry in _parsedNeighbors!.asMap().entries)
|
||||
_buildInfoRow(
|
||||
entry.value['contact'] != null
|
||||
? entry.value['contact'].name
|
||||
@@ -430,6 +441,7 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
double snr,
|
||||
int spreadingFactor,
|
||||
) {
|
||||
final snrUi = snrUiFromSNR(snr, spreadingFactor);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 3),
|
||||
child: Row(
|
||||
@@ -443,9 +455,15 @@ class _NeighboursScreenState extends State<NeighboursScreen> {
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
subtitle: Text(value),
|
||||
trailing: SNRIcon(
|
||||
snr: snr,
|
||||
snrLevels: getSNRfromSF(spreadingFactor),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(snrUi.icon, color: snrUi.color, size: 18.0),
|
||||
Text(
|
||||
snrUi.text,
|
||||
style: TextStyle(fontSize: 10, color: snrUi.color),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -0,0 +1,833 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.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/services/app_settings_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:provider/provider.dart';
|
||||
|
||||
double getPathDistanceMeters(List<LatLng> points) {
|
||||
if (points.length <= 1) return 0.0;
|
||||
|
||||
double distanceMeters = 0.0;
|
||||
final distanceCalculator = Distance();
|
||||
|
||||
for (int i = 0; i < points.length - 1; i++) {
|
||||
distanceMeters += distanceCalculator(points[i], points[i + 1]);
|
||||
}
|
||||
|
||||
return distanceMeters;
|
||||
}
|
||||
|
||||
String formatDistance(double distanceMeters, {required bool isImperial}) {
|
||||
if (isImperial) {
|
||||
return '(${(distanceMeters / 1609.34).toStringAsFixed(2)} mi)';
|
||||
}
|
||||
return '(${(distanceMeters / 1000).toStringAsFixed(2)} km)';
|
||||
}
|
||||
|
||||
class PathTraceData {
|
||||
final Uint8List pathData;
|
||||
final List<double> snrData;
|
||||
final Map<int, Contact> pathContacts;
|
||||
|
||||
PathTraceData({
|
||||
required this.pathData,
|
||||
required this.snrData,
|
||||
required this.pathContacts,
|
||||
});
|
||||
}
|
||||
|
||||
class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
final int? repeaterId;
|
||||
final bool flipPathRound;
|
||||
final bool reversePathRound;
|
||||
final Contact? targetContact;
|
||||
|
||||
const PathTraceMapScreen({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.path,
|
||||
this.repeaterId,
|
||||
this.flipPathRound = false,
|
||||
this.reversePathRound = false,
|
||||
this.targetContact,
|
||||
});
|
||||
|
||||
@override
|
||||
State<PathTraceMapScreen> createState() => _PathTraceMapScreenState();
|
||||
}
|
||||
|
||||
class _PathTraceMapScreenState extends State<PathTraceMapScreen> {
|
||||
static const double _labelZoomThreshold = 8.5;
|
||||
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
Timer? _timeoutTimer;
|
||||
|
||||
bool _isLoading = false;
|
||||
bool _failed2Loaded = false;
|
||||
bool _hasData = false;
|
||||
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<Polyline> _polylines = [];
|
||||
LatLng? _initialCenter = LatLng(0, 0);
|
||||
double _initialZoom = 2.0;
|
||||
LatLngBounds? _bounds;
|
||||
ValueKey<String> _mapKey = const ValueKey('initial');
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_setupFrameListener();
|
||||
_doPathTrace();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_frameSubscription?.cancel();
|
||||
_timeoutTimer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Uint8List addReturnPath(Uint8List pathBytes) {
|
||||
Uint8List? traceBytes;
|
||||
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;
|
||||
}
|
||||
|
||||
Future<void> _doPathTrace() async {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_failed2Loaded = false;
|
||||
});
|
||||
}
|
||||
|
||||
final Uint8List path;
|
||||
|
||||
Uint8List pathTmp = widget.reversePathRound
|
||||
? Uint8List.fromList(widget.path.reversed.toList())
|
||||
: widget.path;
|
||||
|
||||
if (widget.flipPathRound) {
|
||||
path = addReturnPath(pathTmp);
|
||||
} else {
|
||||
path = pathTmp;
|
||||
}
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final frame = buildTraceReq(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0, //flags
|
||||
0, //auth
|
||||
payload: path,
|
||||
);
|
||||
connector.sendFrame(frame);
|
||||
}
|
||||
|
||||
void _setupFrameListener() {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
Uint8List tagData = Uint8List(4);
|
||||
// Listen for incoming text messages from the repeater
|
||||
_frameSubscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty) return;
|
||||
final frameBuffer = BufferReader(frame);
|
||||
try {
|
||||
final code = frameBuffer.readUInt8();
|
||||
|
||||
if (code == respCodeSent) {
|
||||
frameBuffer.skipBytes(1); //reserved
|
||||
tagData = frameBuffer.readBytes(4);
|
||||
final timeoutMilliseconds = frameBuffer.readUInt32LE();
|
||||
|
||||
// Start timeout timer for trace response
|
||||
_timeoutTimer?.cancel();
|
||||
_timeoutTimer = Timer(
|
||||
Duration(milliseconds: timeoutMilliseconds),
|
||||
() {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (code == respCodeErr) {
|
||||
_timeoutTimer?.cancel();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
}
|
||||
|
||||
// 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();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
// Handle any parsing errors gracefully
|
||||
appLogger.error('Error parsing frame: $e', tag: 'PathTraceMapScreen');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleTraceResponse(Uint8List frame) async {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
final buffer = BufferReader(frame);
|
||||
try {
|
||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||
int pathLength = buffer.readUInt8();
|
||||
buffer.skipBytes(5); // Skip Flag byte and tag data
|
||||
buffer.skipBytes(4); // Skip auth code
|
||||
Uint8List pathData = buffer.readBytes(pathLength);
|
||||
List<double> snrData = buffer
|
||||
.readRemainingBytes()
|
||||
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
|
||||
connector.contacts.where((c) => c.type != advTypeChat).forEach((
|
||||
repeater,
|
||||
) {
|
||||
for (var repeaterData in pathData) {
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
final Map<int, LatLng> inferredPositions = {};
|
||||
for (final hop in pathData) {
|
||||
final contact = pathContacts[hop];
|
||||
if (contact != null && contact.hasLocation) continue;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
inferredPositions[hop] = LatLng(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_hasData = true;
|
||||
_inferredHopPositions = inferredPositions;
|
||||
_traceData = PathTraceData(
|
||||
pathData: pathData,
|
||||
snrData: snrData,
|
||||
pathContacts: pathContacts,
|
||||
);
|
||||
// Compute endpoint position for the target contact.
|
||||
LatLng? targetPos;
|
||||
bool targetGuessed = false;
|
||||
final target = widget.targetContact;
|
||||
if (target != null) {
|
||||
if (target.hasLocation) {
|
||||
targetPos = LatLng(target.latitude!, target.longitude!);
|
||||
} else if (pathData.isNotEmpty) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathRound), the target-side hop sits
|
||||
// in the middle of the symmetric sequence; .last is the local side.
|
||||
final lastHop = (widget.flipPathRound && pathData.length > 1)
|
||||
? pathData[(pathData.length - 1) ~/ 2]
|
||||
: pathData.last;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
c.path.last == lastHop,
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
final lat =
|
||||
peers.map((c) => c.latitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (target.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
lat + offsetDeg * cos(angle),
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
_targetContactPosition = targetPos;
|
||||
_targetContactIsGuessed = targetGuessed;
|
||||
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
for (final hop in _traceData!.pathData) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
if (targetPos != null) _points.add(targetPos);
|
||||
_polylines = _points.length > 1
|
||||
? [
|
||||
Polyline(
|
||||
points: _points,
|
||||
strokeWidth: 4,
|
||||
color: Colors.blueAccent,
|
||||
),
|
||||
]
|
||||
: <Polyline>[];
|
||||
|
||||
_initialCenter = _points.isNotEmpty
|
||||
? _points.first
|
||||
: const LatLng(0, 0);
|
||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||
_mapKey = ValueKey(
|
||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||
);
|
||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||
});
|
||||
} catch (e) {
|
||||
appLogger.error(
|
||||
'Error handling trace response: $e',
|
||||
tag: 'PathTraceMapScreen',
|
||||
);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, _) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
child: Text(
|
||||
widget.title,
|
||||
style: const TextStyle(fontSize: 24),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.refresh),
|
||||
onPressed: _isLoading ? null : _doPathTrace,
|
||||
tooltip: context.l10n.pathTrace_refreshTooltip,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Stack(
|
||||
children: [
|
||||
if (!_hasData)
|
||||
Center(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (_isLoading) const CircularProgressIndicator(),
|
||||
const SizedBox(height: 16),
|
||||
if (!_isLoading && _failed2Loaded)
|
||||
Text(context.l10n.pathTrace_notAvailable),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (_hasData) _buildMapPathTrace(context, tileCache),
|
||||
if (_points.isEmpty &&
|
||||
!_hasData &&
|
||||
!_isLoading &&
|
||||
!_failed2Loaded)
|
||||
Center(
|
||||
child: Card(
|
||||
color: Colors.white.withValues(alpha: 0.9),
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: Text(
|
||||
context.l10n.channelPath_noRepeaterLocations,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_hasData)
|
||||
_buildLegendCard(context, _traceData!, isImperial),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
required bool showLabels,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
for (final hop in pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
final inferred = _inferredHopPositions[hop];
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
if (!hasGps && inferred == null) continue;
|
||||
final point = hasGps
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: inferred!;
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
markers.add(
|
||||
Marker(
|
||||
point: point,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: hasGps
|
||||
? Colors.green
|
||||
: Colors.orange.withValues(alpha: 0.75),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hasGps ? label : '~$label',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: contact?.name ?? '~$label',
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final selfLat = context.read<MeshCoreConnector>().selfLatitude;
|
||||
final selfLon = context.read<MeshCoreConnector>().selfLongitude;
|
||||
if (selfLat != null && selfLon != null) {
|
||||
final selfPoint = LatLng(selfLat, selfLon);
|
||||
markers.add(
|
||||
Marker(
|
||||
point: selfPoint,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
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(
|
||||
context.l10n.pathTrace_you,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: selfPoint,
|
||||
label: context.l10n.pathTrace_you,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Add target contact endpoint marker.
|
||||
final targetPos = _targetContactPosition;
|
||||
if (targetPos != null) {
|
||||
final isGuessed = _targetContactIsGuessed;
|
||||
final targetName = widget.targetContact?.name ?? '?';
|
||||
markers.add(
|
||||
Marker(
|
||||
point: targetPos,
|
||||
width: 35,
|
||||
height: 35,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: isGuessed
|
||||
? Colors.purple.withValues(alpha: 0.55)
|
||||
: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Icon(Icons.person, color: Colors.white, size: 18),
|
||||
),
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: targetPos,
|
||||
label: isGuessed ? '~$targetName' : targetName,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return markers;
|
||||
}
|
||||
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
return Marker(
|
||||
point: point,
|
||||
width: 120,
|
||||
height: 24,
|
||||
alignment: Alignment.topCenter,
|
||||
child: IgnorePointer(
|
||||
child: Transform.translate(
|
||||
offset: const Offset(0, -20),
|
||||
child: FittedBox(
|
||||
fit: BoxFit.contain,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black54,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (index == 0) {
|
||||
return context.l10n.pathTrace_you;
|
||||
} else {
|
||||
final contactName = pathTraceData
|
||||
.pathContacts[pathTraceData.pathData[pathTraceData.pathData.length -
|
||||
1]]
|
||||
?.name;
|
||||
final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
} else {
|
||||
final contactName =
|
||||
pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name;
|
||||
final hex = pathTraceData.pathData[index - 1]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
}
|
||||
|
||||
String formatDirectionSubText(PathTraceData pathTraceData, int index) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (index == 0) {
|
||||
final contactName =
|
||||
pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name;
|
||||
final hex = pathTraceData.pathData[0]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
} else {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
} else {
|
||||
final contactName =
|
||||
pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name;
|
||||
final hex = pathTraceData.pathData[index]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildMapPathTrace(
|
||||
BuildContext context,
|
||||
MapTileCacheService tileCache,
|
||||
) {
|
||||
return FlutterMap(
|
||||
key: _mapKey,
|
||||
options: MapOptions(
|
||||
interactionOptions: InteractionOptions(flags: ~InteractiveFlag.rotate),
|
||||
initialCenter: _initialCenter!,
|
||||
initialZoom: _initialZoom,
|
||||
initialCameraFit: _bounds == null
|
||||
? null
|
||||
: CameraFit.bounds(
|
||||
bounds: _bounds!,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: 16,
|
||||
),
|
||||
minZoom: 2.0,
|
||||
maxZoom: 18.0,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (shouldShow != _showNodeLabels && mounted) {
|
||||
setState(() {
|
||||
_showNodeLabels = shouldShow;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
TileLayer(
|
||||
urlTemplate: kMapTileUrlTemplate,
|
||||
tileProvider: tileCache.tileProvider,
|
||||
userAgentPackageName: MapTileCacheService.userAgentPackageName,
|
||||
maxZoom: 19,
|
||||
),
|
||||
if (_polylines.isNotEmpty) PolylineLayer(polylines: _polylines),
|
||||
if (_traceData!.pathData.isNotEmpty)
|
||||
MarkerLayer(
|
||||
markers: _buildHopMarkers(
|
||||
_traceData!.pathData,
|
||||
showLabels: _showNodeLabels,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLegendCard(
|
||||
BuildContext context,
|
||||
PathTraceData pathTraceData,
|
||||
bool isImperial,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
final maxHeight = MediaQuery.of(context).size.height * 0.35;
|
||||
final estimatedHeight = 72.0 + (pathTraceData.pathData.length * 56.0);
|
||||
final cardHeight = max(96.0, min(maxHeight, estimatedHeight));
|
||||
|
||||
return Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: SizedBox(
|
||||
height: cardHeight,
|
||||
child: Card(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Text(
|
||||
'${l10n.channelPath_repeaterHops} ${formatDistance(_pathDistanceMeters, isImperial: isImperial)}',
|
||||
style: const TextStyle(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: pathTraceData.pathData.isEmpty
|
||||
? Center(
|
||||
child: Text(l10n.channelPath_noHopDetailsAvailable),
|
||||
)
|
||||
: Scrollbar(
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: pathTraceData.pathData.length + 1,
|
||||
separatorBuilder: (_, _) => const Divider(height: 1),
|
||||
itemBuilder: (context, index) {
|
||||
final snrUi = snrUiFromSNR(
|
||||
index < pathTraceData.snrData.length
|
||||
? pathTraceData.snrData[index]
|
||||
: null,
|
||||
context.read<MeshCoreConnector>().currentSf,
|
||||
);
|
||||
return Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading:
|
||||
index >= pathTraceData.snrData.length / 2
|
||||
? Icon(Icons.call_received)
|
||||
: Icon(Icons.call_made),
|
||||
title: Text(
|
||||
formatDirectionText(pathTraceData, index),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(
|
||||
formatDirectionSubText(
|
||||
pathTraceData,
|
||||
index,
|
||||
),
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
trailing: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
snrUi.icon,
|
||||
color: snrUi.color,
|
||||
size: 18.0,
|
||||
),
|
||||
Text(
|
||||
snrUi.text,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: snrUi.color,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
// Handle item tap
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -119,14 +119,24 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
|
||||
// Show debug info if requested
|
||||
if (showDebug && mounted) {
|
||||
final frame = buildSendCliCommandFrame(widget.repeater.publicKey, command);
|
||||
DebugFrameViewer.showFrameDebug(context, frame, context.l10n.repeater_cliCommandFrameTitle);
|
||||
final frame = buildSendCliCommandFrame(
|
||||
widget.repeater.publicKey,
|
||||
command,
|
||||
);
|
||||
DebugFrameViewer.showFrameDebug(
|
||||
context,
|
||||
frame,
|
||||
context.l10n.repeater_cliCommandFrameTitle,
|
||||
);
|
||||
}
|
||||
|
||||
// Send CLI command to repeater with retry
|
||||
try {
|
||||
if (_commandService != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final connector = Provider.of<MeshCoreConnector>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
final repeater = _resolveRepeater(connector);
|
||||
final response = await _commandService!.sendCommand(
|
||||
repeater,
|
||||
@@ -158,6 +168,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
|
||||
_commandController.clear();
|
||||
_historyIndex = -1;
|
||||
_commandFocusNode.requestFocus();
|
||||
|
||||
// Auto-scroll to bottom
|
||||
Future.delayed(const Duration(milliseconds: 100), () {
|
||||
@@ -230,7 +241,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
Text(l10n.repeater_cliTitle),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -251,12 +265,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -266,12 +288,20 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -282,7 +312,8 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.bug_report),
|
||||
@@ -473,7 +504,10 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.repeater_enterCommandHint,
|
||||
border: const OutlineInputBorder(),
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
prefixText: '> ',
|
||||
),
|
||||
style: const TextStyle(fontFamily: 'monospace'),
|
||||
@@ -718,10 +752,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
];
|
||||
|
||||
final gpsCommands = [
|
||||
_CommandHelpEntry(
|
||||
command: 'gps',
|
||||
description: l10n.repeater_cliHelpGps,
|
||||
),
|
||||
_CommandHelpEntry(command: 'gps', description: l10n.repeater_cliHelpGps),
|
||||
_CommandHelpEntry(
|
||||
command: 'gps {on|off}',
|
||||
description: l10n.repeater_cliHelpGpsOnOff,
|
||||
@@ -758,13 +789,25 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
style: const TextStyle(fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpSection(context, l10n.repeater_general, generalCommands),
|
||||
_buildHelpSection(
|
||||
context,
|
||||
l10n.repeater_general,
|
||||
generalCommands,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpSection(context, l10n.repeater_settingsCategory, settingsCommands),
|
||||
_buildHelpSection(
|
||||
context,
|
||||
l10n.repeater_settingsCategory,
|
||||
settingsCommands,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpSection(context, l10n.repeater_bridge, bridgeCommands),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpSection(context, l10n.repeater_logging, loggingCommands),
|
||||
_buildHelpSection(
|
||||
context,
|
||||
l10n.repeater_logging,
|
||||
loggingCommands,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildHelpSection(
|
||||
context,
|
||||
@@ -813,10 +856,7 @@ class _RepeaterCliScreenState extends State<RepeaterCliScreen> {
|
||||
),
|
||||
if (note != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
note,
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
Text(note, style: const TextStyle(fontSize: 12)),
|
||||
],
|
||||
const SizedBox(height: 8),
|
||||
...commands.map((entry) => _buildHelpCommandCard(context, entry)),
|
||||
@@ -871,8 +911,5 @@ class _CommandHelpEntry {
|
||||
final String command;
|
||||
final String description;
|
||||
|
||||
const _CommandHelpEntry({
|
||||
required this.command,
|
||||
required this.description,
|
||||
});
|
||||
const _CommandHelpEntry({required this.command, required this.description});
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import 'repeater_status_screen.dart';
|
||||
import 'repeater_cli_screen.dart';
|
||||
import 'repeater_settings_screen.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
import 'neighbours_screen.dart';
|
||||
import 'neighbors_screen.dart';
|
||||
|
||||
class RepeaterHubScreen extends StatelessWidget {
|
||||
final Contact repeater;
|
||||
@@ -21,6 +23,10 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final chemistry = settingsService.batteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
);
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
@@ -73,7 +79,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'<${repeater.publicKeyHex.substring(0, 8)}...${repeater.publicKeyHex.substring(repeater.publicKeyHex.length - 8)}>',
|
||||
repeater.shortPubKeyHex,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey[600]),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
@@ -107,6 +113,62 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.battery_full),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.appSettings_batteryChemistry,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<String>(
|
||||
initialValue: chemistry,
|
||||
isExpanded: true,
|
||||
decoration: const InputDecoration(
|
||||
border: UnderlineInputBorder(),
|
||||
isDense: true,
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
settingsService.setBatteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
value,
|
||||
);
|
||||
},
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 'nmc',
|
||||
child: Text(l10n.appSettings_batteryNmc),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lifepo4',
|
||||
child: Text(l10n.appSettings_batteryLifepo4),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
l10n.repeater_managementTools,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
@@ -174,17 +236,15 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
_buildManagementCard(
|
||||
context,
|
||||
icon: Icons.group,
|
||||
title: l10n.repeater_neighbours,
|
||||
subtitle: l10n.repeater_neighboursSubtitle,
|
||||
title: l10n.repeater_neighbors,
|
||||
subtitle: l10n.repeater_neighborsSubtitle,
|
||||
color: Colors.orange,
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => NeighboursScreen(
|
||||
repeater: repeater,
|
||||
password: password,
|
||||
),
|
||||
builder: (context) =>
|
||||
NeighborsScreen(repeater: repeater, password: password),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -41,7 +41,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
// Basic settings
|
||||
final TextEditingController _nameController = TextEditingController();
|
||||
final TextEditingController _passwordController = TextEditingController();
|
||||
final TextEditingController _guestPasswordController = TextEditingController();
|
||||
final TextEditingController _guestPasswordController =
|
||||
TextEditingController();
|
||||
|
||||
// Radio settings
|
||||
final TextEditingController _freqController = TextEditingController();
|
||||
@@ -60,7 +61,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
bool _privacyMode = false;
|
||||
|
||||
// Advertisement settings
|
||||
bool _advertEnable = true;
|
||||
int _advertInterval = 120; // minutes/2
|
||||
bool _floodAdvertEnable = true;
|
||||
int _floodAdvertInterval = 12; // hours
|
||||
int _privAdvertInterval = 60; // minutes
|
||||
|
||||
@@ -146,7 +149,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
if (_fetchedSettings.isEmpty) return;
|
||||
|
||||
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
||||
appLog.info('Updating UI with keys: ${_fetchedSettings.keys.toList()}', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Updating UI with keys: ${_fetchedSettings.keys.toList()}',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
|
||||
setState(() {
|
||||
// Update name
|
||||
@@ -161,7 +167,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
final radioStr = _fetchedSettings['radio']!;
|
||||
appLog.info('Raw radio string: "$radioStr"', tag: 'RadioSettings');
|
||||
final parts = radioStr.split(',');
|
||||
appLog.info('Split into ${parts.length} parts: $parts', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Split into ${parts.length} parts: $parts',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
|
||||
if (parts.isNotEmpty) {
|
||||
final freqText = parts[0].replaceAll(RegExp(r'[^0-9.]'), '').trim();
|
||||
@@ -193,7 +202,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
appLog.info('CR text: "$crText"', tag: 'RadioSettings');
|
||||
_codingRate = int.tryParse(crText) ?? _codingRate;
|
||||
}
|
||||
appLog.info('Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Final values: freq=${_freqController.text}, bw=$_bandwidth, sf=$_spreadingFactor, cr=$_codingRate',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
}
|
||||
|
||||
if (_fetchedSettings.containsKey('tx')) {
|
||||
@@ -207,11 +219,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
}
|
||||
|
||||
if (_fetchedSettings.containsKey('lat')) {
|
||||
appLog.info('Setting lat to: "${_fetchedSettings['lat']}"', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Setting lat to: "${_fetchedSettings['lat']}"',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
_latController.text = _fetchedSettings['lat']!;
|
||||
}
|
||||
if (_fetchedSettings.containsKey('lon')) {
|
||||
appLog.info('Setting lon to: "${_fetchedSettings['lon']}"', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Setting lon to: "${_fetchedSettings['lon']}"',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
_lonController.text = _fetchedSettings['lon']!;
|
||||
}
|
||||
|
||||
@@ -230,12 +248,14 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_fetchedSettings['advert.interval']!,
|
||||
_advertInterval,
|
||||
);
|
||||
_advertEnable = _advertInterval > 0;
|
||||
}
|
||||
if (_fetchedSettings.containsKey('flood.advert.interval')) {
|
||||
_floodAdvertInterval = _parseIntWithFallback(
|
||||
_fetchedSettings['flood.advert.interval']!,
|
||||
_floodAdvertInterval,
|
||||
);
|
||||
_floodAdvertEnable = _floodAdvertInterval > 0;
|
||||
}
|
||||
if (_fetchedSettings.containsKey('priv.advert.interval')) {
|
||||
_privAdvertInterval = _parseIntWithFallback(
|
||||
@@ -268,7 +288,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
void _applySettingResponse(String command, String response) {
|
||||
final appLog = Provider.of<AppDebugLogService>(context, listen: false);
|
||||
appLog.info('Command: "$command", Raw response: "$response"', tag: 'RadioSettings');
|
||||
appLog.info(
|
||||
'Command: "$command", Raw response: "$response"',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
final value = _extractCliValue(response);
|
||||
appLog.info('Extracted value: "$value"', tag: 'RadioSettings');
|
||||
if (value == null) return;
|
||||
@@ -280,7 +303,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
// Validate response content matches expected format for the command
|
||||
// This prevents mismatched responses over LoRa where order isn't guaranteed
|
||||
if (!_validateResponseForCommand(key, value)) {
|
||||
appLog.warn('Response "$value" does not match expected format for "$key", ignoring', tag: 'RadioSettings');
|
||||
appLog.warn(
|
||||
'Response "$value" does not match expected format for "$key", ignoring',
|
||||
tag: 'RadioSettings',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -311,7 +337,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
// Must have at least 3 commas and start with a frequency-like number
|
||||
final parts = value.split(',');
|
||||
if (parts.length < 4) return false;
|
||||
final freq = double.tryParse(parts[0].replaceAll(RegExp(r'[^0-9.]'), ''));
|
||||
final freq = double.tryParse(
|
||||
parts[0].replaceAll(RegExp(r'[^0-9.]'), ''),
|
||||
);
|
||||
// Frequency should be in reasonable LoRa range (300-2500 MHz)
|
||||
return freq != null && freq >= 300 && freq <= 2500;
|
||||
|
||||
@@ -339,22 +367,33 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
case 'privacy':
|
||||
// Boolean values: on/off/true/false/1/0/enabled/disabled
|
||||
final lower = value.toLowerCase().trim();
|
||||
return ['on', 'off', 'true', 'false', '1', '0', 'enabled', 'disabled'].contains(lower);
|
||||
return [
|
||||
'on',
|
||||
'off',
|
||||
'true',
|
||||
'false',
|
||||
'1',
|
||||
'0',
|
||||
'enabled',
|
||||
'disabled',
|
||||
].contains(lower);
|
||||
|
||||
case 'advert.interval':
|
||||
case 'flood.advert.interval':
|
||||
case 'priv.advert.interval':
|
||||
// Interval: positive integer
|
||||
// Interval: non-negative integer (0 means disabled)
|
||||
if (value.contains(',')) return false;
|
||||
final interval = int.tryParse(value.replaceAll(RegExp(r'[^0-9]'), ''));
|
||||
return interval != null && interval > 0;
|
||||
return interval != null && interval >= 0;
|
||||
|
||||
case 'name':
|
||||
// Name: any non-empty string, but should NOT look like radio settings
|
||||
if (value.isEmpty) return false;
|
||||
// If it has 3+ commas and looks like numbers, probably radio data
|
||||
final commaCount = ','.allMatches(value).length;
|
||||
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) return false;
|
||||
if (commaCount >= 3 && RegExp(r'^[\d.,\s]+$').hasMatch(value)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
default:
|
||||
@@ -551,7 +590,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
final freqMHz = double.tryParse(_freqController.text);
|
||||
if (freqMHz != null) {
|
||||
final bwKHz = _bandwidth! / 1000;
|
||||
commands.add('set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate');
|
||||
commands.add(
|
||||
'set radio ${freqMHz.toStringAsFixed(1)} $bwKHz $_spreadingFactor $_codingRate',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -590,7 +631,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
timestampSeconds: timestampSeconds,
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
await Future.delayed(const Duration(milliseconds: 200)); // Delay between commands
|
||||
await Future.delayed(
|
||||
const Duration(milliseconds: 200),
|
||||
); // Delay between commands
|
||||
}
|
||||
|
||||
setState(() {
|
||||
@@ -614,7 +657,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_errorSavingSettings(e.toString())),
|
||||
content: Text(
|
||||
context.l10n.repeater_errorSavingSettings(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -699,7 +744,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
Text(l10n.repeater_settingsTitle),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -723,12 +771,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -738,12 +794,20 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -754,7 +818,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
if (_hasChanges)
|
||||
TextButton.icon(
|
||||
@@ -865,7 +930,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
border: const OutlineInputBorder(),
|
||||
suffixText: 'MHz',
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
),
|
||||
onChanged: (_) => _markChanged(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -923,10 +990,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: _spreadingFactorOptions.map((sf) {
|
||||
return DropdownMenuItem(
|
||||
value: sf,
|
||||
child: Text('SF$sf'),
|
||||
);
|
||||
return DropdownMenuItem(value: sf, child: Text('SF$sf'));
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
@@ -945,10 +1009,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: _codingRateOptions.map((cr) {
|
||||
return DropdownMenuItem(
|
||||
value: cr,
|
||||
child: Text('4/$cr'),
|
||||
);
|
||||
return DropdownMenuItem(value: cr, child: Text('4/$cr'));
|
||||
}).toList(),
|
||||
onChanged: (value) {
|
||||
if (value != null) {
|
||||
@@ -988,7 +1049,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
helperText: l10n.repeater_latitudeHelper,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
onChanged: (_) => _markChanged(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
@@ -999,7 +1063,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
helperText: l10n.repeater_longitudeHelper,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
keyboardType: const TextInputType.numberWithOptions(decimal: true, signed: true),
|
||||
keyboardType: const TextInputType.numberWithOptions(
|
||||
decimal: true,
|
||||
signed: true,
|
||||
),
|
||||
onChanged: (_) => _markChanged(),
|
||||
),
|
||||
],
|
||||
@@ -1018,11 +1085,17 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.toggle_on, color: Theme.of(context).textTheme.headlineSmall?.color),
|
||||
Icon(
|
||||
Icons.toggle_on,
|
||||
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_features,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1102,7 +1175,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
)
|
||||
: const Icon(Icons.refresh, size: 20),
|
||||
onPressed: isRefreshing ? null : onRefresh,
|
||||
tooltip: refreshTooltip,
|
||||
@@ -1130,40 +1203,72 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
title: Text(l10n.repeater_localAdvertInterval),
|
||||
subtitle: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
|
||||
trailing: Text(l10n.repeater_localAdvertIntervalMinutes(_advertInterval)),
|
||||
subtitle: Text(
|
||||
l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
|
||||
),
|
||||
trailing: Switch(
|
||||
value: _advertEnable,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_advertInterval = value ? 60 : 0;
|
||||
_advertEnable = value;
|
||||
});
|
||||
_markChanged();
|
||||
},
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: _advertInterval.toDouble(),
|
||||
value: _advertInterval == 0
|
||||
? 60.toDouble()
|
||||
: _advertInterval.toDouble(),
|
||||
min: 60,
|
||||
max: 240,
|
||||
divisions: 18,
|
||||
label: l10n.repeater_localAdvertIntervalMinutes(_advertInterval),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_advertInterval = value.toInt();
|
||||
});
|
||||
_markChanged();
|
||||
},
|
||||
onChanged: _advertEnable
|
||||
? (value) {
|
||||
setState(() {
|
||||
_advertInterval = value.toInt();
|
||||
});
|
||||
_markChanged();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ListTile(
|
||||
title: Text(l10n.repeater_floodAdvertInterval),
|
||||
subtitle: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
|
||||
trailing: Text(l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval)),
|
||||
subtitle: Text(
|
||||
l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
|
||||
),
|
||||
trailing: Switch(
|
||||
value: _floodAdvertEnable,
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_floodAdvertInterval = value ? 3 : 0;
|
||||
_floodAdvertEnable = value;
|
||||
});
|
||||
_markChanged();
|
||||
},
|
||||
),
|
||||
),
|
||||
Slider(
|
||||
value: _floodAdvertInterval.toDouble(),
|
||||
value: _floodAdvertInterval == 0
|
||||
? 3.toDouble()
|
||||
: _floodAdvertInterval.toDouble(),
|
||||
min: 3,
|
||||
max: 48,
|
||||
divisions: 45,
|
||||
label: l10n.repeater_floodAdvertIntervalHours(_floodAdvertInterval),
|
||||
onChanged: (value) {
|
||||
setState(() {
|
||||
_floodAdvertInterval = value.toInt();
|
||||
});
|
||||
_markChanged();
|
||||
},
|
||||
max: 168,
|
||||
divisions: 165,
|
||||
label: l10n.repeater_floodAdvertIntervalHours(
|
||||
_floodAdvertInterval,
|
||||
),
|
||||
onChanged: _floodAdvertEnable
|
||||
? (value) {
|
||||
setState(() {
|
||||
_floodAdvertInterval = value.toInt();
|
||||
});
|
||||
_markChanged();
|
||||
}
|
||||
: null,
|
||||
),
|
||||
// Encrypted advertisement interval - hidden until privacy mode is implemented
|
||||
// if (_privacyMode) ...[
|
||||
@@ -1220,10 +1325,15 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
const Divider(),
|
||||
ListTile(
|
||||
leading: Icon(Icons.refresh, color: colorScheme.onErrorContainer),
|
||||
title: Text(l10n.repeater_rebootRepeater, style: TextStyle(color: colorScheme.onErrorContainer)),
|
||||
title: Text(
|
||||
l10n.repeater_rebootRepeater,
|
||||
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||
),
|
||||
subtitle: Text(
|
||||
l10n.repeater_rebootRepeaterSubtitle,
|
||||
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
onTap: () => _confirmAction(
|
||||
l10n.repeater_rebootRepeater,
|
||||
@@ -1246,11 +1356,19 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
// ),
|
||||
// ),
|
||||
ListTile(
|
||||
leading: Icon(Icons.delete_forever, color: colorScheme.onErrorContainer),
|
||||
title: Text(l10n.repeater_eraseFileSystem, style: TextStyle(color: colorScheme.onErrorContainer)),
|
||||
leading: Icon(
|
||||
Icons.delete_forever,
|
||||
color: colorScheme.onErrorContainer,
|
||||
),
|
||||
title: Text(
|
||||
l10n.repeater_eraseFileSystem,
|
||||
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||
),
|
||||
subtitle: Text(
|
||||
l10n.repeater_eraseFileSystemSubtitle,
|
||||
style: TextStyle(color: colorScheme.onErrorContainer.withValues(alpha: 0.8)),
|
||||
style: TextStyle(
|
||||
color: colorScheme.onErrorContainer.withValues(alpha: 0.8),
|
||||
),
|
||||
),
|
||||
onTap: () => _confirmAction(
|
||||
l10n.repeater_eraseFileSystem,
|
||||
@@ -1272,9 +1390,9 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
if (command == 'erase') {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.repeater_eraseSerialOnly)),
|
||||
);
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.repeater_eraseSerialOnly)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,9 @@ import '../models/contact.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/repeater_command_service.dart';
|
||||
import '../utils/battery_utils.dart';
|
||||
import '../widgets/path_management_dialog.dart';
|
||||
|
||||
class RepeaterStatusScreen extends StatefulWidget {
|
||||
@@ -28,7 +30,8 @@ class RepeaterStatusScreen extends StatefulWidget {
|
||||
class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
static const int _statusPayloadOffset = 8;
|
||||
static const int _statusStatsSize = 52;
|
||||
static const int _statusResponseBytes = _statusPayloadOffset + _statusStatsSize;
|
||||
static const int _statusResponseBytes =
|
||||
_statusPayloadOffset + _statusStatsSize;
|
||||
|
||||
bool _isLoading = false;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
@@ -178,6 +181,12 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_dupDirect = directDups;
|
||||
_dupFlood = floodDups;
|
||||
});
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'status_binary',
|
||||
);
|
||||
_recordStatusResult(true);
|
||||
}
|
||||
|
||||
@@ -200,6 +209,18 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
_uptimeSecs = _asInt(data['uptime_secs']);
|
||||
_queueLen = _asInt(data['queue_len']);
|
||||
_debugFlags = _asInt(data['errors']);
|
||||
final batteryMv = _batteryMv;
|
||||
if (batteryMv != null) {
|
||||
final connector = Provider.of<MeshCoreConnector>(
|
||||
context,
|
||||
listen: false,
|
||||
);
|
||||
connector.updateRepeaterBatterySnapshot(
|
||||
widget.repeater.publicKeyHex,
|
||||
batteryMv,
|
||||
source: 'status_text',
|
||||
);
|
||||
}
|
||||
} else if (data.containsKey('noise_floor')) {
|
||||
_noiseFloor = _asInt(data['noise_floor']);
|
||||
_lastRssi = _asInt(data['last_rssi']);
|
||||
@@ -293,7 +314,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(context.l10n.repeater_errorLoadingStatus(e.toString())),
|
||||
content: Text(
|
||||
context.l10n.repeater_errorLoadingStatus(e.toString()),
|
||||
),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -327,7 +350,10 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
Text(l10n.repeater_statusTitle),
|
||||
Text(
|
||||
repeater.name,
|
||||
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.normal),
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -348,12 +374,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
value: 'auto',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.auto_mode, size: 20, color: !isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.auto_mode,
|
||||
size: 20,
|
||||
color: !isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_autoUseSavedPath,
|
||||
style: TextStyle(
|
||||
fontWeight: !isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: !isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -363,12 +397,20 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
value: 'flood',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.waves, size: 20, color: isFloodMode ? Theme.of(context).primaryColor : null),
|
||||
Icon(
|
||||
Icons.waves,
|
||||
size: 20,
|
||||
color: isFloodMode
|
||||
? Theme.of(context).primaryColor
|
||||
: null,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_forceFloodMode,
|
||||
style: TextStyle(
|
||||
fontWeight: isFloodMode ? FontWeight.bold : FontWeight.normal,
|
||||
fontWeight: isFloodMode
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -379,7 +421,8 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
IconButton(
|
||||
icon: const Icon(Icons.timeline),
|
||||
tooltip: l10n.repeater_pathManagement,
|
||||
onPressed: () => PathManagementDialog.show(context, contact: repeater),
|
||||
onPressed: () =>
|
||||
PathManagementDialog.show(context, contact: repeater),
|
||||
),
|
||||
IconButton(
|
||||
icon: _isLoading
|
||||
@@ -423,11 +466,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.info_outline, color: Theme.of(context).textTheme.headlineSmall?.color),
|
||||
Icon(
|
||||
Icons.info_outline,
|
||||
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_systemInformation,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -453,18 +502,30 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.radio, color: Theme.of(context).textTheme.headlineSmall?.color),
|
||||
Icon(
|
||||
Icons.radio,
|
||||
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_radioStatistics,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const Divider(),
|
||||
_buildInfoRow(l10n.repeater_lastRssi, _formatValue(_lastRssi, suffix: ' dB')),
|
||||
_buildInfoRow(
|
||||
l10n.repeater_lastRssi,
|
||||
_formatValue(_lastRssi, suffix: ' dB'),
|
||||
),
|
||||
_buildInfoRow(l10n.repeater_lastSnr, _formatSnr(_lastSnr)),
|
||||
_buildInfoRow(l10n.repeater_noiseFloor, _formatValue(_noiseFloor, suffix: ' dB')),
|
||||
_buildInfoRow(
|
||||
l10n.repeater_noiseFloor,
|
||||
_formatValue(_noiseFloor, suffix: ' dB'),
|
||||
),
|
||||
_buildInfoRow(l10n.repeater_txAirtime, _formatDuration(_txAirSecs)),
|
||||
_buildInfoRow(l10n.repeater_rxAirtime, _formatDuration(_rxAirSecs)),
|
||||
],
|
||||
@@ -483,11 +544,17 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(Icons.analytics, color: Theme.of(context).textTheme.headlineSmall?.color),
|
||||
Icon(
|
||||
Icons.analytics,
|
||||
color: Theme.of(context).textTheme.headlineSmall?.color,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
l10n.repeater_packetStatistics,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -543,25 +610,32 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
}
|
||||
|
||||
String _batteryText() {
|
||||
if (_batteryMv == null) return '—';
|
||||
final percent = _batteryPercentFromMv(_batteryMv!);
|
||||
final volts = (_batteryMv! / 1000.0).toStringAsFixed(2);
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final batteryMv =
|
||||
connector.getRepeaterBatteryMillivolts(widget.repeater.publicKeyHex) ??
|
||||
_batteryMv;
|
||||
if (batteryMv == null) return '—';
|
||||
final percent = estimateBatteryPercentFromMillivolts(
|
||||
batteryMv,
|
||||
_batteryChemistry(),
|
||||
);
|
||||
final volts = (batteryMv / 1000.0).toStringAsFixed(2);
|
||||
return '$percent% / ${volts}V';
|
||||
}
|
||||
|
||||
int _batteryPercentFromMv(int millivolts) {
|
||||
const minMv = 3000;
|
||||
const maxMv = 4200;
|
||||
if (millivolts <= minMv) return 0;
|
||||
if (millivolts >= maxMv) return 100;
|
||||
return (((millivolts - minMv) * 100) / (maxMv - minMv)).round();
|
||||
String _batteryChemistry() {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
return settingsService.batteryChemistryForRepeater(
|
||||
widget.repeater.publicKeyHex,
|
||||
);
|
||||
}
|
||||
|
||||
String _clockText() {
|
||||
if (_statusRequestedAt == null) return '—';
|
||||
final dt = _statusRequestedAt!;
|
||||
final date = '${dt.day}/${dt.month}/${dt.year}';
|
||||
final time = '${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
final time =
|
||||
'${dt.hour.toString().padLeft(2, '0')}:${dt.minute.toString().padLeft(2, '0')}';
|
||||
return '$date $time';
|
||||
}
|
||||
|
||||
@@ -598,7 +672,9 @@ class _RepeaterStatusScreenState extends State<RepeaterStatusScreen> {
|
||||
final direct = _formatValue(_dupDirect);
|
||||
return l10n.repeater_duplicatesFloodDirect(flood, direct);
|
||||
}
|
||||
if (_packetsRecv == null || _floodRx == null || _directRx == null) return '—';
|
||||
if (_packetsRecv == null || _floodRx == null || _directRx == null) {
|
||||
return '—';
|
||||
}
|
||||
final dupTotal = _packetsRecv! - _floodRx! - _directRx!;
|
||||
if (dupTotal < 0) return '—';
|
||||
return l10n.repeater_duplicatesTotal(dupTotal);
|
||||
|
||||
+194
-46
@@ -1,21 +1,101 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../utils/platform_info.dart';
|
||||
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../utils/app_logger.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/device_tile.dart';
|
||||
import 'contacts_screen.dart';
|
||||
import 'usb_screen.dart';
|
||||
|
||||
/// Screen for scanning and connecting to MeshCore devices
|
||||
class ScannerScreen extends StatelessWidget {
|
||||
class ScannerScreen extends StatefulWidget {
|
||||
const ScannerScreen({super.key});
|
||||
|
||||
@override
|
||||
State<ScannerScreen> createState() => _ScannerScreenState();
|
||||
}
|
||||
|
||||
class _ScannerScreenState extends State<ScannerScreen> {
|
||||
bool _changedNavigation = false;
|
||||
late final MeshCoreConnector _connector;
|
||||
late final VoidCallback _connectionListener;
|
||||
BluetoothAdapterState _bluetoothState = BluetoothAdapterState.unknown;
|
||||
late StreamSubscription<BluetoothAdapterState> _bluetoothStateSubscription;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
|
||||
_connectionListener = () {
|
||||
final isCurrentRoute = ModalRoute.of(context)?.isCurrent ?? true;
|
||||
if (_connector.state == MeshCoreConnectionState.disconnected) {
|
||||
_changedNavigation = false;
|
||||
} else if (_connector.state == MeshCoreConnectionState.connected &&
|
||||
_connector.activeTransport == MeshCoreTransportType.bluetooth &&
|
||||
isCurrentRoute &&
|
||||
!_changedNavigation) {
|
||||
_changedNavigation = true;
|
||||
if (mounted) {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (context) => const ContactsScreen()),
|
||||
);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
_connector.addListener(_connectionListener);
|
||||
|
||||
_bluetoothStateSubscription = FlutterBluePlus.adapterState.listen(
|
||||
(state) {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_bluetoothState = state;
|
||||
});
|
||||
// Cancel scan if Bluetooth turns off while scanning
|
||||
if (state != BluetoothAdapterState.on) {
|
||||
unawaited(_connector.stopScan());
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (Object e) {
|
||||
appLogger.warn('Adapter state stream error: $e', tag: 'ScannerScreen');
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connector.removeListener(_connectionListener);
|
||||
unawaited(_bluetoothStateSubscription.cancel());
|
||||
if (!_changedNavigation) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
unawaited(_connector.disconnect(manual: true));
|
||||
});
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final canPop = Navigator.of(context).canPop();
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(context.l10n.scanner_title),
|
||||
leading: canPop
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
onPressed: () {
|
||||
appLogger.info('Back button pressed', tag: 'ScannerScreen');
|
||||
Navigator.of(context).maybePop();
|
||||
},
|
||||
)
|
||||
: null,
|
||||
title: AdaptiveAppBarTitle(context.l10n.scanner_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
),
|
||||
@@ -25,41 +105,82 @@ class ScannerScreen extends StatelessWidget {
|
||||
builder: (context, connector, child) {
|
||||
return Column(
|
||||
children: [
|
||||
// Bluetooth off warning
|
||||
if (_bluetoothState == BluetoothAdapterState.off)
|
||||
_bluetoothOffWarning(context),
|
||||
|
||||
// Status bar
|
||||
_buildStatusBar(context, connector),
|
||||
|
||||
// Device list
|
||||
Expanded(
|
||||
child: _buildDeviceList(context, connector),
|
||||
),
|
||||
Expanded(child: _buildDeviceList(context, connector)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
floatingActionButton: Consumer<MeshCoreConnector>(
|
||||
bottomNavigationBar: Consumer<MeshCoreConnector>(
|
||||
builder: (context, connector, child) {
|
||||
final isScanning = connector.state == MeshCoreConnectionState.scanning;
|
||||
|
||||
return FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
connector.startScan();
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(isScanning ? context.l10n.scanner_stop : context.l10n.scanner_scan),
|
||||
final isScanning =
|
||||
connector.state == MeshCoreConnectionState.scanning;
|
||||
final isBluetoothOff = _bluetoothState == BluetoothAdapterState.off;
|
||||
final usbSupported = PlatformInfo.supportsUsbSerial;
|
||||
|
||||
return SafeArea(
|
||||
top: false,
|
||||
minimum: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (usbSupported)
|
||||
FloatingActionButton.extended(
|
||||
onPressed: () {
|
||||
appLogger.info(
|
||||
'USB selected, opening UsbScreen',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute(builder: (_) => const UsbScreen()),
|
||||
);
|
||||
},
|
||||
heroTag: 'scanner_usb_action',
|
||||
icon: const Icon(Icons.usb),
|
||||
label: Text(context.l10n.connectionChoiceUsbLabel),
|
||||
),
|
||||
if (usbSupported) const SizedBox(width: 12),
|
||||
FloatingActionButton.extended(
|
||||
heroTag: 'scanner_ble_action',
|
||||
onPressed: isBluetoothOff
|
||||
? null
|
||||
: () {
|
||||
if (isScanning) {
|
||||
connector.stopScan();
|
||||
} else {
|
||||
unawaited(
|
||||
connector.startScan().catchError((e) {
|
||||
appLogger.warn(
|
||||
'startScan error: $e',
|
||||
tag: 'ScannerScreen',
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
},
|
||||
icon: isScanning
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.bluetooth_searching),
|
||||
label: Text(
|
||||
isScanning
|
||||
? context.l10n.scanner_stop
|
||||
: context.l10n.scanner_scan,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -70,7 +191,7 @@ class ScannerScreen extends StatelessWidget {
|
||||
String statusText;
|
||||
Color statusColor;
|
||||
|
||||
final l10n = context.l10n;
|
||||
final l10n = context.l10n;
|
||||
switch (connector.state) {
|
||||
case MeshCoreConnectionState.scanning:
|
||||
statusText = l10n.scanner_scanning;
|
||||
@@ -117,20 +238,13 @@ final l10n = context.l10n;
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.bluetooth,
|
||||
size: 64,
|
||||
color: Colors.grey[400],
|
||||
),
|
||||
Icon(Icons.bluetooth, size: 64, color: Colors.grey[400]),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
connector.state == MeshCoreConnectionState.scanning
|
||||
? context.l10n.scanner_searchingDevices
|
||||
: context.l10n.scanner_tapToScan,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
style: TextStyle(fontSize: 16, color: Colors.grey[600]),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -161,15 +275,6 @@ final l10n = context.l10n;
|
||||
? result.device.platformName
|
||||
: result.advertisementData.advName;
|
||||
await connector.connect(result.device, displayName: name);
|
||||
|
||||
if (context.mounted && connector.isConnected) {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ContactsScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
@@ -181,4 +286,47 @@ final l10n = context.l10n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Widget _bluetoothOffWarning(BuildContext context) {
|
||||
final errorColor = Theme.of(context).colorScheme.error;
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
|
||||
color: errorColor.withValues(alpha: 0.15),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.bluetooth_disabled, size: 24, color: errorColor),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.scanner_bluetoothOff,
|
||||
style: TextStyle(
|
||||
color: errorColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
context.l10n.scanner_bluetoothOffMessage,
|
||||
style: TextStyle(
|
||||
color: errorColor.withValues(alpha: 0.85),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (PlatformInfo.isAndroid)
|
||||
TextButton(
|
||||
onPressed: () => FlutterBluePlus.turnOn(),
|
||||
child: Text(context.l10n.scanner_enableBluetooth),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:meshcore_open/utils/gpx_export.dart';
|
||||
import 'package:meshcore_open/widgets/elements_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
@@ -7,6 +8,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
import 'app_settings_screen.dart';
|
||||
import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
@@ -20,6 +22,7 @@ class SettingsScreen extends StatefulWidget {
|
||||
|
||||
class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool _showBatteryVoltage = false;
|
||||
bool _deviceInfoExpanded = false;
|
||||
String _appVersion = '';
|
||||
|
||||
@override
|
||||
@@ -39,7 +42,13 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(l10n.settings_title), centerTitle: true),
|
||||
appBar: AppBar(
|
||||
title: AppBarTitle(
|
||||
l10n.settings_title,
|
||||
indicators: false,
|
||||
subtitle: false,
|
||||
),
|
||||
),
|
||||
body: SafeArea(
|
||||
top: false,
|
||||
child: Consumer<MeshCoreConnector>(
|
||||
@@ -57,6 +66,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
const SizedBox(height: 16),
|
||||
_buildDebugCard(context),
|
||||
const SizedBox(height: 16),
|
||||
_buildExportCard(connector),
|
||||
const SizedBox(height: 16),
|
||||
_buildAboutCard(context),
|
||||
],
|
||||
);
|
||||
@@ -71,43 +82,84 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
|
||||
return Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settings_deviceInfo,
|
||||
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildInfoRow(l10n.settings_infoName, connector.deviceDisplayName),
|
||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoStatus,
|
||||
connector.isConnected
|
||||
? l10n.common_connected
|
||||
: l10n.common_disconnected,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InkWell(
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_deviceInfoExpanded = !_deviceInfoExpanded;
|
||||
});
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.settings_deviceInfo,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
AnimatedRotation(
|
||||
turns: _deviceInfoExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Icon(Icons.expand_more),
|
||||
),
|
||||
],
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoContactsCount,
|
||||
'${connector.contacts.length}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoChannelCount,
|
||||
'${connector.channels.length}',
|
||||
),
|
||||
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox.shrink(),
|
||||
secondChild: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoName,
|
||||
connector.deviceDisplayName,
|
||||
),
|
||||
_buildInfoRow(l10n.settings_infoId, connector.deviceIdLabel),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoStatus,
|
||||
connector.isConnected
|
||||
? l10n.common_connected
|
||||
: l10n.common_disconnected,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.selfName != null)
|
||||
_buildInfoRow(l10n.settings_nodeName, connector.selfName!),
|
||||
if (connector.selfPublicKey != null)
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoPublicKey,
|
||||
'${pubKeyToHex(connector.selfPublicKey!).substring(0, 16)}...',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoContactsCount,
|
||||
'${connector.contacts.length}',
|
||||
),
|
||||
_buildInfoRow(
|
||||
l10n.settings_infoChannelCount,
|
||||
'${connector.channels.length}',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
crossFadeState: _deviceInfoExpanded
|
||||
? CrossFadeState.showSecond
|
||||
: CrossFadeState.showFirst,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -225,6 +277,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () => _editLocation(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.group_add_outlined),
|
||||
title: Text(l10n.settings_contactSettings),
|
||||
subtitle: Text(l10n.settings_contactSettingsSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () => _editAutoAddConfig(context, connector),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.visibility_off_outlined),
|
||||
title: Text(l10n.settings_privacyMode),
|
||||
@@ -352,22 +412,33 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Color? valueColor,
|
||||
VoidCallback? onTap,
|
||||
}) {
|
||||
final theme = Theme.of(context);
|
||||
|
||||
final row = Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (leading != null) ...[leading, const SizedBox(width: 8)],
|
||||
Text(label, style: TextStyle(color: Colors.grey[600])),
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: theme.colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: Text(
|
||||
value,
|
||||
style: TextStyle(fontWeight: FontWeight.w500, color: valueColor),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
value,
|
||||
style: theme.textTheme.bodyLarge?.copyWith(
|
||||
color: valueColor,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -376,11 +447,12 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
if (onTap != null) {
|
||||
return InkWell(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: row,
|
||||
);
|
||||
}
|
||||
|
||||
return row;
|
||||
}
|
||||
|
||||
@@ -442,7 +514,8 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
bool isGPSEnabled = customVars["gps"] == "1";
|
||||
|
||||
// Read current interval or default to 900 (15 minutes)
|
||||
final currentInterval = int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
final currentInterval =
|
||||
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
intervalController.text = currentInterval.toString();
|
||||
|
||||
showDialog(
|
||||
@@ -683,6 +756,225 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _gpxExport(
|
||||
GpxExport exporter,
|
||||
String name,
|
||||
String description,
|
||||
String filename,
|
||||
String shareText,
|
||||
String subject,
|
||||
) async {
|
||||
final l10n = context.l10n;
|
||||
final result = await exporter.exportGPX(
|
||||
name,
|
||||
description,
|
||||
filename,
|
||||
shareText,
|
||||
subject,
|
||||
);
|
||||
if (!mounted) return;
|
||||
switch (result) {
|
||||
case gpxExportSuccess:
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportSuccess)));
|
||||
case gpxExportNoContacts:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_gpxExportNoContacts)),
|
||||
);
|
||||
break;
|
||||
case gpxExportNotAvailable:
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_gpxExportNotAvailable)),
|
||||
);
|
||||
break;
|
||||
case gpxExportFailed:
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_gpxExportError)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildExportCard(MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
return Card(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(l10n.settings_gpxExportRepeaters),
|
||||
subtitle: Text(l10n.settings_gpxExportRepeatersSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final exporter = GpxExport(connector);
|
||||
exporter.addRepeaters();
|
||||
_gpxExport(
|
||||
exporter,
|
||||
l10n.map_repeater,
|
||||
l10n.settings_gpxExportRepeatersRoom,
|
||||
"meshcore_repeaters_",
|
||||
l10n.settings_gpxExportShareText,
|
||||
l10n.settings_gpxExportShareSubject,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(l10n.settings_gpxExportContacts),
|
||||
subtitle: Text(l10n.settings_gpxExportContactsSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final exporter = GpxExport(connector);
|
||||
exporter.addContacts();
|
||||
_gpxExport(
|
||||
exporter,
|
||||
l10n.map_repeater,
|
||||
l10n.settings_gpxExportChat,
|
||||
"meshcore_contacts_",
|
||||
l10n.settings_gpxExportShareText,
|
||||
l10n.settings_gpxExportShareSubject,
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.download_outlined),
|
||||
title: Text(l10n.settings_gpxExportAll),
|
||||
subtitle: Text(l10n.settings_gpxExportAllSubtitle),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
onTap: () async {
|
||||
final exporter = GpxExport(connector);
|
||||
exporter.addAll();
|
||||
_gpxExport(
|
||||
exporter,
|
||||
l10n.map_repeater,
|
||||
l10n.settings_gpxExportAllContacts,
|
||||
"meshcore_all_",
|
||||
l10n.settings_gpxExportShareText,
|
||||
l10n.settings_gpxExportShareSubject,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editAutoAddConfig(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
bool autoAddChat = false;
|
||||
bool autoAddRepeater = false;
|
||||
bool autoAddRoomServer = false;
|
||||
bool autoAddSensor = false;
|
||||
bool overwriteOldest = false;
|
||||
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
autoAddChat = connector.autoAddUsers ?? false;
|
||||
autoAddRepeater = connector.autoAddRepeaters ?? false;
|
||||
autoAddRoomServer = connector.autoAddRoomServers ?? false;
|
||||
autoAddSensor = connector.autoAddSensors ?? false;
|
||||
overwriteOldest = connector.autoAddOverwriteOldest ?? false;
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.contactsSettings_autoAddTitle),
|
||||
content: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FeatureToggleRow(
|
||||
title: l10n.contactsSettings_autoAddUsersTitle,
|
||||
subtitle: l10n.contactsSettings_autoAddUsersSubtitle,
|
||||
value: autoAddChat,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => autoAddChat = value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
FeatureToggleRow(
|
||||
title: l10n.contactsSettings_autoAddRepeatersTitle,
|
||||
subtitle: l10n.contactsSettings_autoAddRepeatersSubtitle,
|
||||
value: autoAddRepeater,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => autoAddRepeater = value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
FeatureToggleRow(
|
||||
title: l10n.contactsSettings_autoAddRoomServersTitle,
|
||||
subtitle: l10n.contactsSettings_autoAddRoomServersSubtitle,
|
||||
value: autoAddRoomServer,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => autoAddRoomServer = value);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
FeatureToggleRow(
|
||||
title: l10n.contactsSettings_autoAddSensorsTitle,
|
||||
subtitle: l10n.contactsSettings_autoAddSensorsSubtitle,
|
||||
value: autoAddSensor,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => autoAddSensor = value);
|
||||
},
|
||||
),
|
||||
Divider(height: 4),
|
||||
FeatureToggleRow(
|
||||
title: l10n.contactsSettings_overwriteOldestTitle,
|
||||
subtitle: l10n.contactsSettings_overwriteOldestSubtitle,
|
||||
value: overwriteOldest,
|
||||
onChanged: (value) {
|
||||
setDialogState(() => overwriteOldest = value);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
_sendSettings(
|
||||
connector,
|
||||
autoAddChat,
|
||||
autoAddRepeater,
|
||||
autoAddRoomServer,
|
||||
autoAddSensor,
|
||||
overwriteOldest,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _sendSettings(
|
||||
MeshCoreConnector connector,
|
||||
bool autoAddChat,
|
||||
bool autoAddRepeater,
|
||||
bool autoAddRoomServer,
|
||||
bool autoAddSensor,
|
||||
bool overwriteOldest,
|
||||
) async {
|
||||
final frame = buildSetAutoAddConfigFrame(
|
||||
autoAddChat: autoAddChat,
|
||||
autoAddRepeater: autoAddRepeater,
|
||||
autoAddRoomServer: autoAddRoomServer,
|
||||
autoAddSensor: autoAddSensor,
|
||||
overwriteOldest: overwriteOldest,
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
await connector.sendFrame(buildGetAutoAddFlagsFrame());
|
||||
}
|
||||
}
|
||||
|
||||
class _RadioSettingsDialog extends StatefulWidget {
|
||||
@@ -700,6 +992,7 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
LoRaSpreadingFactor _spreadingFactor = LoRaSpreadingFactor.sf7;
|
||||
LoRaCodingRate _codingRate = LoRaCodingRate.cr4_5;
|
||||
final _txPowerController = TextEditingController(text: '20');
|
||||
bool _clientRepeat = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -749,6 +1042,8 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
if (widget.connector.currentTxPower != null) {
|
||||
_txPowerController.text = widget.connector.currentTxPower.toString();
|
||||
}
|
||||
|
||||
_clientRepeat = widget.connector.clientRepeat ?? false;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -780,10 +1075,13 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (txPower == null || txPower < 0 || txPower > 22) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.settings_txPowerInvalid)));
|
||||
final maxTxPower = widget.connector.maxTxPower ?? 22;
|
||||
if (txPower == null || txPower < 0 || txPower > maxTxPower) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${l10n.settings_txPowerInvalid} (0-$maxTxPower dBm)'),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -795,9 +1093,29 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
widget.connector.currentCr,
|
||||
);
|
||||
|
||||
// if the client repeat isnt null then we know its supported
|
||||
//otherwise we leave it out of the frame to avoid accidentally enabling
|
||||
final knownRepeat = widget.connector.clientRepeat != null;
|
||||
|
||||
if (knownRepeat) {
|
||||
const validRepeatFreqsKHz = {433000, 869000, 918000};
|
||||
if (_clientRepeat && !validRepeatFreqsKHz.contains(freqHz)) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(l10n.settings_clientRepeatFreqWarning)),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await widget.connector.sendFrame(
|
||||
buildSetRadioParamsFrame(freqHz, bwHz, sf, cr),
|
||||
buildSetRadioParamsFrame(
|
||||
freqHz,
|
||||
bwHz,
|
||||
sf,
|
||||
cr,
|
||||
clientRepeat: knownRepeat ? _clientRepeat : null,
|
||||
),
|
||||
);
|
||||
await widget.connector.sendFrame(buildSetRadioTxPowerFrame(txPower));
|
||||
await widget.connector.refreshDeviceInfo();
|
||||
@@ -836,37 +1154,25 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
l10n.settings_presets,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
children: [
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset915Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset915MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset868Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset868MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_preset433Mhz,
|
||||
onTap: () => _applyPreset(RadioSettings.preset433MHz),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_longRange,
|
||||
onTap: () => _applyPreset(RadioSettings.presetLongRange),
|
||||
),
|
||||
_PresetChip(
|
||||
label: l10n.settings_fastSpeed,
|
||||
onTap: () => _applyPreset(RadioSettings.presetFastSpeed),
|
||||
),
|
||||
DropdownButtonFormField<int>(
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_presets,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
for (var i = 0; i < RadioSettings.presets.length; i++)
|
||||
DropdownMenuItem(
|
||||
value: i,
|
||||
child: Text(RadioSettings.presets[i].$1),
|
||||
),
|
||||
],
|
||||
onChanged: (index) {
|
||||
if (index != null) {
|
||||
_applyPreset(RadioSettings.presets[index].$2);
|
||||
}
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _frequencyController,
|
||||
decoration: InputDecoration(
|
||||
@@ -932,10 +1238,22 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.settings_txPower,
|
||||
border: const OutlineInputBorder(),
|
||||
helperText: l10n.settings_txPowerHelper,
|
||||
helperText: widget.connector.maxTxPower != null
|
||||
? '${l10n.settings_txPowerHelper} (max: ${widget.connector.maxTxPower} dBm)'
|
||||
: l10n.settings_txPowerHelper,
|
||||
),
|
||||
keyboardType: TextInputType.number,
|
||||
),
|
||||
if (widget.connector.clientRepeat != null) ...[
|
||||
const SizedBox(height: 16),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.settings_clientRepeat),
|
||||
subtitle: Text(l10n.settings_clientRepeatSubtitle),
|
||||
value: _clientRepeat,
|
||||
onChanged: (value) => setState(() => _clientRepeat = value),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -949,15 +1267,3 @@ class _RadioSettingsDialogState extends State<_RadioSettingsDialog> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PresetChip extends StatelessWidget {
|
||||
final String label;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _PresetChip({required this.label, required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ActionChip(label: Text(label), onPressed: onTap);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user