Compare commits
123 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c78c77309e | |||
| 724696cd3c | |||
| cf6c93c50a | |||
| eee72b4c65 | |||
| 736eb064bf | |||
| 9e6968aa04 | |||
| 8904fc7208 | |||
| 02d3325d0d | |||
| 3a08f52fd4 | |||
| 3a9c14e946 | |||
| b15f6450bd | |||
| 2879bf6147 | |||
| 36f12ebded | |||
| 98f6cf3745 | |||
| 35e4d7fa6c | |||
| 0fe2502309 | |||
| a8cac76491 | |||
| 2df17be673 | |||
| 71fa8c17a4 | |||
| 698a69dc03 | |||
| 8765d0f4cc | |||
| c683dbd8f1 | |||
| b8e113d269 | |||
| e963248e37 | |||
| 68d2250afc | |||
| f4cca2fb1d | |||
| 0f3961f3a2 | |||
| 08130350d7 | |||
| c1dea86dd1 | |||
| e1b8d6ec97 | |||
| 3541b1bbac | |||
| ef6c1e0898 | |||
| eb89ad1d50 | |||
| 5ef976b3b5 | |||
| 7b4f90d646 | |||
| e1a4900dc0 | |||
| 7fc253cc88 | |||
| 445a18a83f | |||
| 8273c416b5 | |||
| b44c69f411 | |||
| 6492058a04 | |||
| 5670ab0067 | |||
| 98a9197cb1 | |||
| 55da952bfd | |||
| 9b4742951d | |||
| 43f3d439ba | |||
| ed294decfb | |||
| 5f54a2cd1b | |||
| e1536c49b1 | |||
| bfa62523df | |||
| 6813a72767 | |||
| dbd3a40bdc | |||
| 3009036565 | |||
| 80f4fd5218 | |||
| 321d4b9775 | |||
| 1c183d7e67 | |||
| a1e6f6967c | |||
| f34c2a92c3 | |||
| f1478722b0 | |||
| c867225073 | |||
| 06285a02ff | |||
| e92d215582 | |||
| d61eaaaff1 | |||
| ecde5e1109 | |||
| f548f95e29 | |||
| 27695d372a | |||
| d06a4c1861 | |||
| c46a5ffa2f | |||
| a15f5e94da | |||
| 9d1251f21e | |||
| 0ca96ce5b1 | |||
| b72ade1387 | |||
| 3fc038682c | |||
| 9e5b360f28 | |||
| 89360c8bc2 | |||
| 28df5a97e8 | |||
| 2e767730fd | |||
| b7fe5331d0 | |||
| 61fb39b9f8 | |||
| 9e18b99e9d | |||
| 2e4f26d7cd | |||
| 6e81fdfd46 | |||
| 5d03f99040 | |||
| e88281b7b6 | |||
| 3a11e35a7a | |||
| 29e551d6cb | |||
| 531b85b8aa | |||
| 3d956596a5 | |||
| bdd0d3dee2 | |||
| 11a4f9a373 | |||
| acbe4dcc49 | |||
| 9b99eadcf7 | |||
| 09a83c4f22 | |||
| 5486ae8ccb | |||
| fc67d8d5aa | |||
| 225d07b440 | |||
| 945a118853 | |||
| 01d4f002f8 | |||
| 9331b02f94 | |||
| 27f7932db9 | |||
| 4a39a7cd46 | |||
| 3f1bd54889 | |||
| 91759f531a | |||
| 87708e2684 | |||
| 73e5b80232 | |||
| 28a5913ba3 | |||
| 091c754584 | |||
| cf27c59c84 | |||
| 119590434a | |||
| dc82414447 | |||
| 23f29f2cda | |||
| d66b16a4e8 | |||
| d6647f4701 | |||
| 87b0fd6fc7 | |||
| 4a066e08dc | |||
| 94ed729985 | |||
| 8296f44a4e | |||
| 4e70fd2a5e | |||
| ca02197414 | |||
| 6e2abfeb2d | |||
| b62c1c7338 | |||
| 17379394bd | |||
| 0e074fd806 |
@@ -0,0 +1,3 @@
|
||||
github: [zjs81]
|
||||
custom:
|
||||
- "https://paypal.me/zjs82"
|
||||
@@ -48,7 +48,7 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
- **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
|
||||
- **Offline Maps**: Download map tiles for offline use in remote areas
|
||||
- **Offline Maps**: Download map tiles for offline use in remote areas (with [StadiaMaps](https://stadiamaps.com/pricing/) Free Subscription API-Key)
|
||||
- **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
|
||||
|
||||
### Device Management
|
||||
@@ -224,6 +224,10 @@ Messages are transmitted as binary frames using a custom protocol optimized for
|
||||
|
||||
This is an open-source project. Contributions are welcome!
|
||||
|
||||
## SWHID and Archive badge
|
||||
[](https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/zjs81/meshcore-open)
|
||||
[](https://archive.softwareheritage.org/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55;origin=https://github.com/zjs81/meshcore-open;visit=swh:1:snp:47656c4b55ab40a689ff8d2f045196725f05096b;anchor=swh:1:rev:0fe250230905fdd05dbedc0f546736990beacf53)
|
||||
|
||||
### Development Guidelines
|
||||
|
||||
- Follow the Flutter style guide
|
||||
|
||||
@@ -43,9 +43,22 @@ android {
|
||||
// arguments += listOf("-DANDROID_STL=c++_shared")
|
||||
// }
|
||||
// }
|
||||
// ndk {
|
||||
// abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64")
|
||||
// }
|
||||
// arm64-v8a only, deliberately.
|
||||
//
|
||||
// * ONNX Runtime (flutter_onnxruntime, used by the AEIC-SE image codec)
|
||||
// ships a per-ABI .so. arm64-v8a alone costs ~18 MB of APK; a
|
||||
// universal APK carrying armeabi-v7a and x86_64 as well costs ~56 MB.
|
||||
// * llamadart only declares android-arm64 and android-x64 backends in
|
||||
// pubspec.yaml's `hooks.user_defines`, so an armeabi-v7a build already
|
||||
// has no translation backend at all.
|
||||
// * The image codec needs ~2.7 GiB peak resident, which no 32-bit
|
||||
// address space can provide regardless of ABI.
|
||||
//
|
||||
// Consequence: this APK will not install on 32-bit-only ARM devices or on
|
||||
// x86_64 emulators. For emulator work, temporarily add "x86_64" here.
|
||||
ndk {
|
||||
abiFilters += listOf("arm64-v8a")
|
||||
}
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -67,6 +80,13 @@ android {
|
||||
} else {
|
||||
signingConfigs.getByName("debug")
|
||||
}
|
||||
// ONNX Runtime resolves its Java classes from native code by name.
|
||||
// Without these rules R8 renames them and the process SIGABRTs with
|
||||
// "java_class == null" the instant the codec runs a model.
|
||||
proguardFiles(
|
||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||
"proguard-rules.pro",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
# ONNX Runtime looks its Java classes up from native code with FindClass /
|
||||
# GetMethodID, by literal name. R8 renames them, the lookup returns null, and
|
||||
# the process dies with:
|
||||
#
|
||||
# JNI DETECTED ERROR IN APPLICATION: java_class == null
|
||||
# at art::JNI<false>::GetMethodID
|
||||
# from convertToTensorInfo -> Java_ai_onnxruntime_OrtSession_run
|
||||
#
|
||||
# It is a hard SIGABRT in native code, so nothing in Dart can catch it: the app
|
||||
# vanishes the moment the codec touches the model. Keep the whole package —
|
||||
# these are the types the native layer reflects on to build tensors and read
|
||||
# results back.
|
||||
-keep class ai.onnxruntime.** { *; }
|
||||
-keepclassmembers class ai.onnxruntime.** { *; }
|
||||
-dontwarn ai.onnxruntime.**
|
||||
|
||||
# The Flutter plugin's platform-channel handler, reached the same way.
|
||||
-keep class com.masicai.flutteronnxruntime.** { *; }
|
||||
-dontwarn com.masicai.flutteronnxruntime.**
|
||||
@@ -22,7 +22,7 @@
|
||||
<uses-feature android:name="android.hardware.usb.host" android:required="false"/>
|
||||
|
||||
<application
|
||||
android:label="meshcore_open"
|
||||
android:label="MeshCore Open"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
<service
|
||||
|
||||
|
After Width: | Height: | Size: 37 KiB |
|
After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 183 KiB |
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground>
|
||||
<inset
|
||||
android:drawable="@drawable/ic_launcher_foreground"
|
||||
android:inset="16%" />
|
||||
</foreground>
|
||||
</adaptive-icon>
|
||||
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_launcher_background">#9F9D95</color>
|
||||
</resources>
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
|
||||
This license is copied below, and is also available with a FAQ at: https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
After Width: | Height: | Size: 669 KiB |
@@ -14,7 +14,8 @@ MeshCore Open is an open-source Flutter client for MeshCore LoRa mesh networking
|
||||
8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
|
||||
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors
|
||||
10. [Additional Features](additional-features.md) - GIF picker, localization, debug logs, SMAZ compression, and more
|
||||
11. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||
11. [Routing Paths](routing-paths.md) - Path encoding, validation, device capability detection, and storage
|
||||
12. [BLE Protocol & Data Layer](ble-protocol.md) - Technical reference for the communication protocol and data architecture
|
||||
|
||||
## App Overview
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ An ML-based service that predicts expected delivery timeouts:
|
||||
- Applies a **1.5x safety margin** to raw predictions (the actual timeout issued is 1.5× the model's predicted delivery time)
|
||||
- Features with zero variance are automatically excluded from training
|
||||
- Blends per-contact statistics with ML predictions
|
||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data
|
||||
- Falls back to `3000 + 3000 × pathLength` ms when insufficient data. Note: `pathLength` here refers to the stored hop count in the app's model/storage (number of hops), not the on-air encoded byte length.
|
||||
- Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
|
||||
|
||||
---
|
||||
|
||||
@@ -120,6 +120,8 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||
| 54 | CMD_SET_FLOOD_SCOPE | Set flood routing scope (v8+) |
|
||||
| 55 | CMD_SEND_CONTROL_DATA | Send control data (e.g. zero-hop discovery, v8+) |
|
||||
| 56 | CMD_GET_STATS | Request companion radio stats |
|
||||
| 57 | CMD_SEND_ANON_REQ | Send anonymous request |
|
||||
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
|
||||
@@ -162,6 +164,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
|
||||
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
|
||||
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
|
||||
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
|
||||
|
||||
## Data Models
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
# Routing Paths
|
||||
|
||||
This page covers how MeshCore Open represents, selects, validates, and stores routing paths in the UI and data layer.
|
||||
|
||||
## Path Routing
|
||||
|
||||
MeshCore supports variable-length multi-byte routing paths so the app can scale from small meshes to very large node sets.
|
||||
|
||||
### Hash Width and Multi-Byte Paths
|
||||
|
||||
The device capability determines the hash width (number of bytes per hop):
|
||||
|
||||
| Width | Max Unique Nodes | Typical Use |
|
||||
|-------|-----------------|-------------|
|
||||
| 1 byte | 256 | Single-byte node IDs |
|
||||
| 2 bytes | 65,536 | Medium meshes |
|
||||
| 3 bytes | 16.7M | Large networks |
|
||||
| 4 bytes | 4.3G | Very large meshes / future-proofing |
|
||||
|
||||
### Device Capability Detection
|
||||
|
||||
On device connection, the app reads the firmware capability to set `pathHashByteWidth`:
|
||||
|
||||
```dart
|
||||
// Read from device info response (offset 81)
|
||||
final modeRaw = firmwareBytes.length >= 82
|
||||
? (firmwareBytes[81] & 0xFF)
|
||||
: 0;
|
||||
final mode = modeRaw.clamp(0, 3);
|
||||
_pathHashByteWidth = mode + 1; // 1, 2, 3, or 4 bytes per hop
|
||||
```
|
||||
|
||||
The connector reads a single-mode byte and clamps to `0..3`, so the supported hop width is `1..4` bytes. UI code also clamps widths when rendering (typically to `1..4`) so 4-byte hops are handled end-to-end in the current codebase.
|
||||
|
||||
### Path Data Structure
|
||||
|
||||
Paths in messages and storage consist of:
|
||||
|
||||
- **`pathLength` (model/storage)**: Hop count (number of hops). Negative values (e.g. `-1`) are used as a flood sentinel.
|
||||
- **On-air `path_len` byte**: A packed byte that encodes hop count + hash width and is decoded into `pathLength` + `pathBytes` when parsing frames.
|
||||
- **`pathBytes`**: Raw bytes of the path (concatenated hop prefixes), grouped by `pathHashByteWidth`.
|
||||
- **`hopCount`**: Derived display value computed from bytes and width: `(byteCount + hashByteWidth - 1) ~/ hashByteWidth`.
|
||||
- **Example**: With `pathHashByteWidth=2`, a 3-hop path has 6 bytes (`pathBytes.length = 6`) and `pathLength = 3`:
|
||||
- `pathBytes = [0xA1, 0xA2, 0xB1, 0xB2, 0xC1, 0xC2]`
|
||||
- Hops: `[0xA1A2]`, `[0xB1B2]`, `[0xC1C2]`
|
||||
|
||||
### Hop Count Calculation
|
||||
|
||||
Convert path byte length to hop count:
|
||||
|
||||
```dart
|
||||
int hopCount = (byteCount + hashByteWidth - 1) ~/ hashByteWidth;
|
||||
```
|
||||
|
||||
Use this consistently when displaying hop counts in UI. Do not treat `pathLength` as a hop count when the path uses multi-byte hop hashes.
|
||||
|
||||
### Path Usage in Different Message Types
|
||||
|
||||
- **Direct messages**: Extract path from decrypted payload to trace sender route.
|
||||
- **Channel messages**: Decrypt hop-by-hop routing chain from payload; the header carries the encoded byte length for the path blob, not the derived hop count.
|
||||
- **Contact storage**: Path length in byte 32, raw path bytes in bytes 33-96, grouped by detected `pathHashByteWidth`.
|
||||
|
||||
### UI Hop Count Display
|
||||
|
||||
⚠️ **Important**: In screens like "Channel Message Path", prefer the actual decoded `pathBytes` hop count over `pathLength` metadata:
|
||||
|
||||
```dart
|
||||
// Preferred: use actual observed path length
|
||||
final effectiveHopCount = (pathBytes.length + width - 1) ~/ width;
|
||||
|
||||
// Avoid: using encoded byte length as if it were a hop count
|
||||
// pathLength is bytes; converting it twice causes inflated counts
|
||||
```
|
||||
|
||||
Example scenario:
|
||||
- Radio header reports `pathLength: 32` bytes
|
||||
- Decoded path bytes: `[0xAB, 0xCD]` (2 bytes = 1 hop with width=2)
|
||||
- **Display**: "1 hop" (from `pathBytes`), not "32 hops" (which would double-count the encoded length)
|
||||
|
||||
@@ -52,7 +52,7 @@ These settings are sent directly to the connected device firmware.
|
||||
|
||||
### Radio Settings
|
||||
Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
|
||||
- **Frequency** (MHz): Free text, validated 300–2500 MHz
|
||||
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
|
||||
- **Spreading Factor**: SF5–SF12
|
||||
@@ -129,6 +129,10 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **Raster Tile Source**: Sets the MAP theme and with that from where to get the map tile data:
|
||||
- OpenStreetMap (Auto/Standard/Dark) is provided by the free OpenStreetMap tile server. (Can only be used for live view or already cached.)
|
||||
- Stamen Terrain / AlidadeSmooth Dark / Outdoors / OSM Bright are [StadiaMaps.com raster tile maps](https://stadiamaps.com/products/maps/interactive-basemaps/) for which you can choose an Hosted Endpoint (Worldwide / Europe hosted) and have to provide the API key to your subscription. (You can cache these maps for offline Map usage.)
|
||||
There will be no account provided by meshcore-open. You will have to get you own subscription. StadiaMaps offers a [free](https://stadiamaps.com/pricing/) subscription to download up to 200'000 Standart Raster Basemap tiles.
|
||||
- **Offline Map Cache**: Navigate to tile download screen
|
||||
|
||||
### Translation
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>method</key>
|
||||
<string>app-store-connect</string>
|
||||
<key>teamID</key>
|
||||
<string>X74VJ4UAST</string>
|
||||
<key>signingStyle</key>
|
||||
<string>automatic</string>
|
||||
<key>uploadSymbols</key>
|
||||
<true/>
|
||||
<key>destination</key>
|
||||
<string>export</string>
|
||||
</dict>
|
||||
</plist>
|
||||
@@ -1,73 +1,21 @@
|
||||
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
|
||||
- mobile_scanner (7.0.0):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- package_info_plus (0.4.5):
|
||||
- Flutter
|
||||
- share_plus (0.0.1):
|
||||
- Flutter
|
||||
- shared_preferences_foundation (0.0.1):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- sqflite_darwin (0.0.4):
|
||||
- Flutter
|
||||
- FlutterMacOS
|
||||
- url_launcher_ios (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/darwin`)
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_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`)
|
||||
|
||||
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/darwin"
|
||||
package_info_plus:
|
||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_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"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
||||
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89
|
||||
flutter_local_notifications: a5a732f069baa862e728d839dd2ebb904737effb
|
||||
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
|
||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
|
||||
PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
|
||||
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
|
||||
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||
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 */; };
|
||||
@@ -52,6 +53,7 @@
|
||||
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>"; };
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||
@@ -67,6 +69,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -85,6 +88,7 @@
|
||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||
@@ -180,13 +184,15 @@
|
||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
|
||||
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = Runner;
|
||||
packageProductDependencies = (
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||
);
|
||||
productName = Runner;
|
||||
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@@ -220,6 +226,9 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 97C146E51CF9000F007C117D;
|
||||
packageReferences = (
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
|
||||
);
|
||||
productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@@ -283,39 +292,6 @@
|
||||
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;
|
||||
};
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||
);
|
||||
name = "Fix Native Asset Minimum OS";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||
};
|
||||
DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
@@ -338,6 +314,22 @@
|
||||
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;
|
||||
};
|
||||
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputPaths = (
|
||||
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}",
|
||||
);
|
||||
name = "Fix Native Asset Minimum OS";
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "set -e\nFRAMEWORKS_DIR=\"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}\"\nMIN_OS=\"${IPHONEOS_DEPLOYMENT_TARGET}\"\nif [ ! -d \"$FRAMEWORKS_DIR\" ] || [ -z \"$MIN_OS\" ]; then\n exit 0\nfi\nfind \"$FRAMEWORKS_DIR\" -maxdepth 2 -name Info.plist | while read -r plist; do\n bundle_id=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' \"$plist\" 2>/dev/null || true)\n case \"$bundle_id\" in\n io.flutter.flutter.native-assets.*)\n /usr/libexec/PlistBuddy -c \"Set :MinimumOSVersion $MIN_OS\" \"$plist\" 2>/dev/null || \\\n /usr/libexec/PlistBuddy -c \"Add :MinimumOSVersion string $MIN_OS\" \"$plist\"\n ;;\n esac\ndone\n";
|
||||
};
|
||||
/* End PBXShellScriptBuildPhase section */
|
||||
|
||||
/* Begin PBXSourcesBuildPhase section */
|
||||
@@ -446,7 +438,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -625,7 +619,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -647,7 +643,9 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
|
||||
DEVELOPMENT_TEAM = X74VJ4UAST;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -696,6 +694,20 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCLocalSwiftPackageReference section */
|
||||
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */ = {
|
||||
isa = XCLocalSwiftPackageReference;
|
||||
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCLocalSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
productName = FlutterGeneratedPluginSwiftPackage;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
};
|
||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,24 @@
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
<PreActions>
|
||||
<ExecutionAction
|
||||
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||
<ActionContent
|
||||
title = "Run Prepare Flutter Framework Script"
|
||||
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||
<EnvironmentBuildable>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||
BuildableName = "Runner.app"
|
||||
BlueprintName = "Runner"
|
||||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</EnvironmentBuildable>
|
||||
</ActionContent>
|
||||
</ExecutionAction>
|
||||
</PreActions>
|
||||
<BuildActionEntries>
|
||||
<BuildActionEntry
|
||||
buildForTesting = "YES"
|
||||
|
||||
@@ -37,6 +37,8 @@
|
||||
<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>NSPhotoLibraryUsageDescription</key>
|
||||
<string>This app lets you pick a photo to compress and send over the mesh.</string>
|
||||
<key>UIApplicationSceneManifest</key>
|
||||
<dict>
|
||||
<key>UIApplicationSupportsMultipleScenes</key>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
// Buffer Reader - sequential binary data reader with pointer tracking
|
||||
@@ -206,6 +207,8 @@ const int cmdSendTelemetryReq = 39;
|
||||
const int cmdGetCustomVar = 40;
|
||||
const int cmdSetCustomVar = 41;
|
||||
const int cmdSendBinaryReq = 50;
|
||||
const int cmdSetFloodScope = 54;
|
||||
const int cmdSendControlData = 55;
|
||||
const int cmdGetStats = 56;
|
||||
const int cmdSendAnonReq = 57;
|
||||
const int cmdSetAutoAddConfig = 58;
|
||||
@@ -230,6 +233,12 @@ Uint8List buildTelemetryBinaryPayload() {
|
||||
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]);
|
||||
}
|
||||
|
||||
const int anonReqTypeRegions = 0x01;
|
||||
|
||||
// Control data sub-types used by MeshCore discovery packets.
|
||||
const int controlSubtypeDiscoverReq = 0x08;
|
||||
const int controlSubtypeDiscoverResp = 0x09;
|
||||
|
||||
// Repeater response codes
|
||||
const int respServerLoginOk = 0;
|
||||
|
||||
@@ -272,6 +281,7 @@ const int pushCodeTraceData = 0x89;
|
||||
const int pushCodeNewAdvert = 0x8A;
|
||||
const int pushCodeTelemetryResponse = 0x8B;
|
||||
const int pushCodeBinaryResponse = 0x8C;
|
||||
const int pushCodeControlData = 0x8E;
|
||||
|
||||
// Contact/advertisement types
|
||||
const int advTypeChat = 1;
|
||||
@@ -577,9 +587,9 @@ Uint8List buildGetStatsFrame(int statsType) {
|
||||
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]);
|
||||
}
|
||||
|
||||
/// Path hash width on air: [61][0][mode], mode 0..2 → (mode+1) bytes per hop hash.
|
||||
/// Path hash width on air: [61][0][mode], mode 0..3 → (mode+1) bytes per hop hash.
|
||||
Uint8List buildSetPathHashModeFrame(int mode) {
|
||||
final m = mode.clamp(0, 2);
|
||||
final m = mode.clamp(0, 3).toInt();
|
||||
return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
|
||||
}
|
||||
|
||||
@@ -866,6 +876,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List buildSendControlDataFrame(Uint8List payload) {
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendControlData);
|
||||
writer.writeBytes(payload);
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List buildDiscoveryRequestPayload(
|
||||
int tag, {
|
||||
bool prefixOnly = false,
|
||||
int typeMask = 1 << advTypeRepeater,
|
||||
}) {
|
||||
final writer = BufferWriter();
|
||||
// The high bit must be set for CMD_SEND_CONTROL_DATA; DISCOVER_REQ uses
|
||||
// subtype 0x8, with the low bit selecting short/full public keys in replies.
|
||||
writer.writeByte(
|
||||
(controlSubtypeDiscoverReq << 4) | (prefixOnly ? 0x01 : 0x00),
|
||||
);
|
||||
writer.writeByte(typeMask);
|
||||
writer.writeUInt32LE(tag);
|
||||
writer.writeUInt32LE(0); // since=0 asks nearby nodes for any recent advert.
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
Uint8List _reversePathByHop(Uint8List path, int pathHashWidth) {
|
||||
if (path.isEmpty) return Uint8List(0);
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
if (path.length % width != 0) {
|
||||
return Uint8List.fromList(path.reversed.toList());
|
||||
}
|
||||
|
||||
final reversed = Uint8List(path.length);
|
||||
final hops = path.length ~/ width;
|
||||
for (var i = 0; i < hops; i++) {
|
||||
final from = (hops - 1 - i) * width;
|
||||
reversed.setRange(i * width, (i + 1) * width, path, from);
|
||||
}
|
||||
return reversed;
|
||||
}
|
||||
|
||||
// Build CMD_SEND_ANON_REQ frame.
|
||||
// Payload format for regions: [anon_req_type][reply_path_len][reply_path...].
|
||||
Uint8List buildSendAnonReqFrame(
|
||||
Uint8List repeaterPubKey, {
|
||||
required int requestType,
|
||||
Uint8List? replyPath,
|
||||
int replyHopCount = 0,
|
||||
int pathHashWidth = pathHashSize,
|
||||
}) {
|
||||
final width = pathHashWidth.clamp(1, 4).toInt();
|
||||
final path = replyPath ?? Uint8List(0);
|
||||
final encodedPathLen = ((width - 1) << 6) | (replyHopCount & 0x3F);
|
||||
final writer = BufferWriter();
|
||||
writer.writeByte(cmdSendAnonReq);
|
||||
writer.writeBytes(repeaterPubKey);
|
||||
writer.writeByte(requestType);
|
||||
writer.writeByte(encodedPathLen);
|
||||
writer.writeBytes(_reversePathByHop(path, width));
|
||||
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}) {
|
||||
@@ -960,3 +1031,18 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
|
||||
}
|
||||
return writer.toBytes();
|
||||
}
|
||||
|
||||
//Build CMD_SET_FLOOD_SCOPE
|
||||
// Format: [cmd][scope]
|
||||
Uint8List buildSetFloodScopeFrame(String region) {
|
||||
if (region == '') {
|
||||
// reset scope
|
||||
return Uint8List.fromList([cmdSetFloodScope, 0]);
|
||||
}
|
||||
|
||||
final name = region.startsWith('#') ? region : '#$region';
|
||||
final hash = crypto.sha256.convert(utf8.encode(name)).bytes;
|
||||
final scope = Uint8List.fromList(hash.sublist(0, 16));
|
||||
|
||||
return Uint8List.fromList([cmdSetFloodScope, 0, ...scope]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
|
||||
import '../l10n/l10n.dart';
|
||||
import '../widgets/image_send_codec_binding.dart';
|
||||
import '../widgets/image_send_preview_sheet.dart';
|
||||
|
||||
/// Shared "attach an image" flow, used by both the direct-message and channel
|
||||
/// chat screens so the two surfaces cannot drift apart.
|
||||
///
|
||||
/// Picks a photo, then shows [ImageSendPreviewSheet] so the user sees the
|
||||
/// packet count and airtime *before* committing to a send that may occupy the
|
||||
/// channel for seconds. Returns null if the user backed out at either step.
|
||||
///
|
||||
/// The caller owns the actual transmission: this only produces the payload.
|
||||
Future<ImageSendPreviewResult?> pickAndPreviewImage({
|
||||
required BuildContext context,
|
||||
required ImageSendCodec codec,
|
||||
ImagePicker? picker,
|
||||
}) async {
|
||||
final XFile? picked;
|
||||
try {
|
||||
picked = await (picker ?? ImagePicker()).pickImage(
|
||||
source: ImageSource.gallery,
|
||||
// The codec centre-crops to 512x512 anyway, so there is nothing to gain
|
||||
// from decoding a 12 MP original -- but stay well above 512 so the crop
|
||||
// still has detail to work with.
|
||||
maxWidth: 2048,
|
||||
maxHeight: 2048,
|
||||
);
|
||||
} on Exception catch (e) {
|
||||
debugPrint('image pick failed: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (picked == null) return null; // user cancelled the picker
|
||||
|
||||
final Uint8List bytes;
|
||||
int originalBytes;
|
||||
try {
|
||||
bytes = await picked.readAsBytes();
|
||||
originalBytes = bytes.length;
|
||||
if (!kIsWeb) {
|
||||
// length() is the on-disk size, which is what we want to show against the
|
||||
// transmitted size; fall back to the in-memory length if it fails.
|
||||
try {
|
||||
originalBytes = await File(picked.path).length();
|
||||
} on FileSystemException {
|
||||
// keep the in-memory length
|
||||
}
|
||||
}
|
||||
} on Exception catch (e) {
|
||||
debugPrint('image read failed: $e');
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.chat_imagePickFailed)),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!context.mounted) return null;
|
||||
|
||||
return showImageSendPreviewSheet(
|
||||
context: context,
|
||||
imageBytes: bytes,
|
||||
originalFileBytes: originalBytes,
|
||||
codec: codec,
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_linkify/flutter_linkify.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
@@ -20,6 +21,7 @@ class LinkHandler {
|
||||
required String text,
|
||||
required TextStyle style,
|
||||
TextStyle? linkStyle,
|
||||
VoidCallback? onSecondaryTap,
|
||||
}) {
|
||||
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
|
||||
const options = LinkifyOptions(humanize: false, defaultToHttps: false);
|
||||
@@ -27,7 +29,7 @@ class LinkHandler {
|
||||
void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
|
||||
|
||||
if (PlatformInfo.isDesktop) {
|
||||
return SelectableLinkify(
|
||||
final linkify = SelectableLinkify(
|
||||
text: text,
|
||||
style: style,
|
||||
linkStyle: effectiveLinkStyle,
|
||||
@@ -35,6 +37,14 @@ class LinkHandler {
|
||||
linkifiers: linkifiers,
|
||||
onOpen: onOpen,
|
||||
);
|
||||
if (onSecondaryTap == null) return linkify;
|
||||
return Listener(
|
||||
onPointerDown: (event) {
|
||||
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
|
||||
},
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: linkify,
|
||||
);
|
||||
}
|
||||
return Linkify(
|
||||
text: text,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import '../models/contact.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../models/contact.dart';
|
||||
|
||||
class PathHelper {
|
||||
static String formatPathHex(List<int> pathBytes) {
|
||||
@@ -12,10 +14,17 @@ class PathHelper {
|
||||
return byte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
static String formatHopHex(List<int> hopBytes) {
|
||||
return hopBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join();
|
||||
}
|
||||
|
||||
static String? hopName(int byte, List<Contact> allContacts) {
|
||||
final matches = allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.publicKey.isNotEmpty &&
|
||||
c.publicKey.first == byte &&
|
||||
(c.type == advTypeRepeater || c.type == advTypeRoom),
|
||||
)
|
||||
@@ -25,12 +34,58 @@ class PathHelper {
|
||||
return matches.map((c) => c.name).join(' | ');
|
||||
}
|
||||
|
||||
static List<Uint8List> splitPathBytes(
|
||||
List<int> pathBytes,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final hops = <Uint8List>[];
|
||||
for (int i = 0; i < pathBytes.length; i += width) {
|
||||
final endIdx = (i + width).clamp(0, pathBytes.length).toInt();
|
||||
final hopBytes = pathBytes.sublist(i, endIdx);
|
||||
if (hopBytes.isNotEmpty) {
|
||||
hops.add(Uint8List.fromList(hopBytes));
|
||||
}
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
/// Resolves path bytes to contact names, supporting multi-byte hash widths.
|
||||
///
|
||||
/// Groups path bytes according to [hashByteWidth]:
|
||||
/// - 1: Single byte per hop (256 unique nodes)
|
||||
/// - 2: Two bytes per hop (65K unique nodes)
|
||||
/// - 3: Three bytes per hop (16M unique nodes)
|
||||
/// - 4: Four bytes per hop (4.3G unique nodes)
|
||||
static String resolvePathNames(
|
||||
List<int> pathBytes,
|
||||
List<Contact> allContacts,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
return pathBytes
|
||||
.map((b) => hopName(b, allContacts) ?? hopHex(b))
|
||||
.join(' \u2192 ');
|
||||
if (pathBytes.isEmpty) return '';
|
||||
|
||||
final parts = <String>[];
|
||||
|
||||
for (final hopBytes in splitPathBytes(pathBytes, hashByteWidth)) {
|
||||
final hex = formatHopHex(hopBytes);
|
||||
|
||||
final matches = allContacts.where((c) {
|
||||
if (c.publicKey.length < hopBytes.length) return false;
|
||||
if (c.type != advTypeRepeater && c.type != advTypeRoom) return false;
|
||||
return listEquals(c.publicKey.sublist(0, hopBytes.length), hopBytes);
|
||||
}).toList();
|
||||
|
||||
if (matches.isEmpty) {
|
||||
parts.add(hex);
|
||||
} else if (matches.length == 1) {
|
||||
parts.add(matches.first.name);
|
||||
} else {
|
||||
parts.add(matches.map((c) => c.name).join(' | '));
|
||||
}
|
||||
}
|
||||
|
||||
return parts.join(' \u2192 ');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import 'path_helper.dart';
|
||||
import '../models/contact.dart';
|
||||
|
||||
class PathHopResolver {
|
||||
@@ -11,30 +12,35 @@ class PathHopResolver {
|
||||
required List<Contact> contacts,
|
||||
LatLng? endpoint,
|
||||
bool resolveFromEnd = false,
|
||||
int pathHashByteWidth = 1,
|
||||
}) {
|
||||
final candidatesByPrefix = <int, List<Contact>>{};
|
||||
final width = pathHashByteWidth.clamp(1, 4).toInt();
|
||||
final candidatesByPrefix = <String, List<Contact>>{};
|
||||
for (final contact in contacts) {
|
||||
if (contact.publicKey.isEmpty) continue;
|
||||
if (contact.publicKey.length < width) continue;
|
||||
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
|
||||
continue;
|
||||
}
|
||||
candidatesByPrefix
|
||||
.putIfAbsent(contact.publicKey.first, () => <Contact>[])
|
||||
.add(contact);
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, width),
|
||||
);
|
||||
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
|
||||
}
|
||||
for (final candidates in candidatesByPrefix.values) {
|
||||
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen));
|
||||
}
|
||||
|
||||
final resolved = List<Contact?>.filled(pathBytes.length, null);
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, width);
|
||||
final resolved = List<Contact?>.filled(hops.length, null);
|
||||
final indexes = resolveFromEnd
|
||||
? List<int>.generate(pathBytes.length, (i) => pathBytes.length - 1 - i)
|
||||
: List<int>.generate(pathBytes.length, (i) => i);
|
||||
? List<int>.generate(hops.length, (i) => hops.length - 1 - i)
|
||||
: List<int>.generate(hops.length, (i) => i);
|
||||
final distance = Distance();
|
||||
var previousPosition = endpoint;
|
||||
|
||||
for (final index in indexes) {
|
||||
final candidates = candidatesByPrefix[pathBytes[index]];
|
||||
final candidates =
|
||||
candidatesByPrefix[PathHelper.formatHopHex(hops[index])];
|
||||
if (candidates == null || candidates.isEmpty) continue;
|
||||
|
||||
var bestIndex = 0;
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Невалидна ширина или дължина.",
|
||||
"settings_latitude": "Широчина",
|
||||
"settings_longitude": "Дължина",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Автоматична zero-hop обява при GPS обновяване",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).",
|
||||
"settings_privacyMode": "Режим на поверителност",
|
||||
"settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите",
|
||||
"settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Последни 6 часа",
|
||||
"appSettings_last24Hours": "Последно 24 часа",
|
||||
"appSettings_lastWeek": "Миналата седмица",
|
||||
"appSettings_rasterTileSource": "Източник на растерни плочки",
|
||||
"appSettings_stadiaEndpoint": "Крайна точка на Stadia",
|
||||
"appSettings_stadiaApiKey": "API ключ за Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Задължително за използване на Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Конфигурирано: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.",
|
||||
"appSettings_offlineMapCache": "Кеш на офлайн карти",
|
||||
"appSettings_noAreaSelected": "Няма избрана област",
|
||||
"appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Без посочено местоположение",
|
||||
"pathMap_followPacket": "Проследи пакета",
|
||||
"pathMap_unfollowPacket": "Спри проследяването на пакета",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Региони",
|
||||
"settings_regionSettingsSubtitle": "Управляне на хранилищни региони",
|
||||
"settings_regionManagement_screenTitle": "Регионално управление",
|
||||
"settings_regionNameHint": "Възразна название на региона",
|
||||
"settings_regionAddRegion": "Добавете регион",
|
||||
"settings_regionFetchRegions": "Извън рептерите от региони",
|
||||
"settings_regionFetchRegionsFail": "Не бъде открити никако региони",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Тата регион е уже добавена",
|
||||
"settings_regionName": "Регионноименоване",
|
||||
"settings_regionDeleted": "Регион отстранен",
|
||||
"settings_deleteRegion": "Удаляне на регион",
|
||||
"settings_deleteRegionConfirm": "Състрясна: отпиште \"{region}\" от списка региони?",
|
||||
"settings_infoHardware": "Оборудование",
|
||||
"settings_infoFirmware": "Програмно-оборонни системни программи (фirmware)",
|
||||
"repeater_pathHashModeOption0": "0 - 1 байт",
|
||||
"repeater_pathHashModeOption1": "1 - 2 байта",
|
||||
"repeater_pathHashModeOption2": "2 - 3 байта",
|
||||
"repeater_pathHashModeOption3": "3 - 4 байта",
|
||||
"appSettings_batteryLipoHv": "LiPo ВЗГ (3,0-4,35 В)",
|
||||
"channels_regionSetTo": "Регион: {region}",
|
||||
"channels_regionNotSet": "Регион: ние",
|
||||
"channels_regionSelect_Title": "Выберете регион",
|
||||
"channels_clearRegion": "Чиста зона",
|
||||
"chat_sendImage": "Изпрати изображение",
|
||||
"chat_imagePickFailed": "Не мог да откривам та изображин",
|
||||
"chat_receivedGif": "Получил GIF",
|
||||
"repeater_keySettings": "Изменяне на идентичности ключа",
|
||||
"repeater_keySettingsSubtitle": "Замени ключовият пар от публични/частни",
|
||||
"repeater_prvKey": "Частни ключ",
|
||||
"repeater_prvKeyHelper": "Нова приватна ключ на повторчето, 128-символовна шестнадатерична строка.",
|
||||
"repeater_generatePrvKey": "Генериране на случайна ключи",
|
||||
"repeater_stopGeneratingPrvKey": "Прервана поискна на ключова парта",
|
||||
"repeater_pubKey": "Публична ключ",
|
||||
"repeater_pubKeyHelper": "Та е публичната ключ, която се свързува с генериранта приватна ключ. Трябва да не настройте тоа напрямо.",
|
||||
"repeater_pubKeyPrefix": "Желата префикс",
|
||||
"repeater_pubKeyPrefixHelper": "Наискате публична ключ, който почева с този шестнадатницил. Ожидавани проби: {tries}.",
|
||||
"imageMessages_enableTitle": "Абilitiraм изображените сообщения",
|
||||
"imageMessages_enableSubtitle": "Изправате изображения чрез сетката. Треба е одноразовна загрузка модел на изображение.",
|
||||
"imageMessages_modelSectionTitle": "Изображени модель",
|
||||
"imageMessages_downloadModel": "Скачане",
|
||||
"imageMessages_cancelDownload": "Отмена",
|
||||
"imageMessages_removeModel": "Удалете модель",
|
||||
"imageMessages_modelReady": "Готов",
|
||||
"imageMessages_modelNotPublished": "Еще не опубликовано — тази версия не може да скане.",
|
||||
"imageMessages_downloadFailed": "Модел за изображение не може да бъде скачан.",
|
||||
"imageMessages_autoProcessTitle": "Автоматически обрабатвате изображения",
|
||||
"imageMessages_autoProcessSubtitle": "Катализиране на всята изображение сразу по пришествие. Използване на около 2 ГБ на втора пъти; откажете от реконструкцията чаке при натискане.",
|
||||
"imageSend_title": "Изпрати изображение",
|
||||
"imageSend_cropNote": "Масштабиран до 512 × 512 · соотношение сторон не съхранясе",
|
||||
"imageSend_originalSize": "Късно на използване на език:",
|
||||
"imageSend_onAirSize": "В ефир",
|
||||
"imageSend_quality": "Качество",
|
||||
"imageSend_qualityStandard": "Стандарт",
|
||||
"imageSend_qualityHigh": "Высок",
|
||||
"imageSend_packetsLabel": "Пакети",
|
||||
"imageSend_airtimeLabel": "Времето на изкъсване",
|
||||
"imageSend_sizeLabel": "Баланса",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{пакет} other{пакета}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Настройки на радио неизвестни",
|
||||
"imageSend_radioUnknownBody": "Усредете се с устройство, за да се отработа часът на изстъпане.",
|
||||
"imageSend_longSendTitle": "Долга пресилка",
|
||||
"imageSend_longSendBody": "Тази буде поддержива открит отряд на около {duration}.",
|
||||
"imageSend_floodNote": "Руйжинг на превъште: всички ретранслятори в диапазонта превратят всички пакети, тъйа каналят оставря дълше, от този час.",
|
||||
"imageSend_parityTitle": "Возобновителен пакет",
|
||||
"imageSend_paritySubtitle": "Едно дополително пакет. Групни извещания не принимаются, след това разбирателят едно пакетто, какщо е пропущен един пакет.",
|
||||
"imageSend_send": "Отправляй",
|
||||
"imageSend_cancel": "Отмена",
|
||||
"imageSend_encodeFailed": "Тази изображение не може да бъде кодирано.",
|
||||
"imageSend_codecDownloading": "Модел на изображение еще разгрузира.",
|
||||
"imageSend_codecUnavailable": "Изглед отправа не е доступен на този устройство.",
|
||||
"imageSend_codecDisabled": "Изображенията на извещенията е отключена в настройките.",
|
||||
"imageSend_deviceUnsupported": "Та радиота не може да пресылава изображени пакети. Подключите устройство, който работи с компанионски прошивка 13 или новия.",
|
||||
"imageSend_directMessagesUnsupported": "Изображенията се превозводя се как групова информация, следато да се отправят единствено на канал — не в лична вестка.",
|
||||
"imageSend_tooLarge": "Таата изображение е кодирано в по-числа пакети, отгоре както формат на сетта разрешава.",
|
||||
"imageSend_sentConfirmation": "Изображението е изпратено като {count} {count, plural, =1{пакет} other{пакета}}.",
|
||||
"imageSend_sendFailed": "Изображение не може да бъде отправено: {error}",
|
||||
"imageSend_sendingProgress": "Отправка наизображение — пакет {sent} от {total}",
|
||||
"receivedImage_senderPrefix": "Узъдето {prefix}",
|
||||
"receivedImage_incoming": "{received} от {total} пакета",
|
||||
"receivedImage_queued": "Ожидането за декодиране",
|
||||
"receivedImage_tapToDecode": "Натискнете за декодиране",
|
||||
"receivedImage_decoding": "Повторно… за приблизително 1 секунда",
|
||||
"receivedImage_incomplete": "Изображение неполно — {received} от {total} пакети отправили",
|
||||
"receivedImage_corrupt": "Изображение не може да бъде воссоздано",
|
||||
"receivedImage_decoderMissing": "Получена изображение — разгадка на изображение е вреjan",
|
||||
"receivedImage_evicted": "Изображение не е хранитено",
|
||||
"receivedImage_retry": "Повтори",
|
||||
"receivedImage_decodeAgain": "Правилейте заново",
|
||||
"receivedImage_openSettings": "Настройте",
|
||||
"receivedImage_tapToProcess": "Натискнете за обработка",
|
||||
"receivedImage_awaiting": "{bytes} байта · {packets} {packets, plural, =1{пакет} other{пакета}}",
|
||||
"imageSend_secondsValue": "{seconds} секунди",
|
||||
"imageSend_minutesSecondsValue": "{minutes} мин {seconds} сек"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.",
|
||||
"settings_latitude": "Breitengrad",
|
||||
"settings_longitude": "Längengrad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische Zero-Hop-Ankündigung bei GPS-Update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).",
|
||||
"settings_privacyMode": "Privatsphärenmodus",
|
||||
"settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen",
|
||||
"settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Letzte 6 Stunden",
|
||||
"appSettings_last24Hours": "Letzte 24 Stunden",
|
||||
"appSettings_lastWeek": "Letzte Woche",
|
||||
"appSettings_rasterTileSource": "Quelle für Rasterkacheln",
|
||||
"appSettings_stadiaEndpoint": "Stadia-Endpunkt",
|
||||
"appSettings_stadiaApiKey": "Stadia-API-Schlüssel",
|
||||
"appSettings_stadiaApiKeyRequired": "Für die Nutzung von Stadia Maps erforderlich",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfiguriert: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.",
|
||||
"appSettings_offlineMapCache": "Offline-Karten-Cache",
|
||||
"appSettings_noAreaSelected": "Kein Bereich ausgewählt",
|
||||
"appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})",
|
||||
@@ -791,6 +806,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2488,6 +2553,13 @@
|
||||
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
|
||||
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
|
||||
"repeater_chanUtil": "Nutzung des Kanals",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@routing_lastWorked": {
|
||||
"placeholders": {
|
||||
"when": {
|
||||
@@ -2495,6 +2567,27 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regionen",
|
||||
"settings_regionSettingsSubtitle": "Gespeicherte Regionen verwalten",
|
||||
"settings_regionManagement_screenTitle": "Regions-Verwaltung",
|
||||
"settings_regionNameHint": "Regions-Namen eingeben",
|
||||
"settings_regionAddRegion": "Region hinzufügen",
|
||||
"settings_regionDeleted": "Region entfernt",
|
||||
"settings_regionName": "Regions-Name",
|
||||
"settings_deleteRegion": "Region entfernen",
|
||||
"settings_deleteRegionConfirm": "Region \"{region}\" aus der Liste entfernen?",
|
||||
"channels_regionNotSet": "Region: keine",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"channels_regionSelect_Title": "Region auswählen",
|
||||
"channels_clearRegion": "Region zurücksetzen",
|
||||
"@routing_deliveryCounts": {
|
||||
"placeholders": {
|
||||
"successes": {
|
||||
@@ -2674,5 +2767,199 @@
|
||||
"pathMap_noLocation": "Keine Standortdaten",
|
||||
"pathMap_followPacket": "Ansicht auf Paket fixieren",
|
||||
"pathMap_unfollowPacket": "Fixierung aufheben",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionFetchRegions": "Regionen von Wiederholern abrufen",
|
||||
"settings_regionFetchRegionsFail": "Es wurden keine Regionen gefunden.",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Diese Region wurde bereits hinzugefügt",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 Byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 Bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 Bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 Bytes",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0–4,35 V)",
|
||||
"chat_sendImage": "Bild senden",
|
||||
"chat_imagePickFailed": "Konnte dieses Bild nicht öffnen.",
|
||||
"chat_receivedGif": "Erhalten eines GIF",
|
||||
"repeater_keySettings": "Identitätsschlüssel ändern",
|
||||
"repeater_keySettingsSubtitle": "Ändern Sie das öffentliche/private Schlüsselpaar",
|
||||
"repeater_prvKey": "Privatschlüssel",
|
||||
"repeater_prvKeyHelper": "Ein neuer privater Schlüssel für den Repeater, eine 128-stellige Hexadezimalzeichenkette.",
|
||||
"repeater_generatePrvKey": "Generiere ein zufälliges Schlüsselpaar",
|
||||
"repeater_stopGeneratingPrvKey": "Suchvorgang für Schlüsselpaar unterbrechen",
|
||||
"repeater_pubKey": "Öffentlicher Schlüssel",
|
||||
"repeater_pubKeyHelper": "Dies ist der öffentliche Schlüssel, der zum generierten privaten Schlüssel gehört. Sie können dies nicht direkt festlegen.",
|
||||
"repeater_pubKeyPrefix": "Gewünschtes Präfix",
|
||||
"repeater_pubKeyPrefixHelper": "Suchen Sie einen öffentlichen Schlüssel, der mit diesen Hexadezimalziffern beginnt. Erwartete Versuche: {tries}.",
|
||||
"imageMessages_enableTitle": "Bildnachrichten aktivieren",
|
||||
"imageMessages_enableSubtitle": "Bilder über das Netzwerk senden. Einmaliges Herunterladen eines Bildmodells erforderlich.",
|
||||
"imageMessages_modelSectionTitle": "Bildmodell",
|
||||
"imageMessages_downloadModel": "Herunterladen",
|
||||
"imageMessages_cancelDownload": "Stornieren",
|
||||
"imageMessages_removeModel": "Modell entfernen",
|
||||
"imageMessages_modelReady": "Bereit",
|
||||
"imageMessages_modelNotPublished": "Noch nicht veröffentlicht – dieser Build kann es nicht herunterladen.",
|
||||
"imageMessages_downloadFailed": "Das Bildmodell konnte nicht heruntergeladen werden.",
|
||||
"imageMessages_autoProcessTitle": "Automatisch Bilder verarbeiten",
|
||||
"imageMessages_autoProcessSubtitle": "Rekonstruiere jedes Bild sofort nach dem Eintreffen. Verbraucht etwa 2 GB Arbeitsspeicher für jede zweite Wiederherstellung; deaktiviere die automatische Rekonstruktion mit einem Tippen.",
|
||||
"imageSend_title": "Bild senden",
|
||||
"imageSend_cropNote": "Für 512 × 512 skaliert · Seitenverhältnis nicht erhalten",
|
||||
"imageSend_originalSize": "Originaltext:\n\nThe quick brown fox jumps over the lazy dog.\n\nÜbersetzung:\n\nDer schnelle braune Fuchs springt über den faulen Hund.",
|
||||
"imageSend_onAirSize": "On air",
|
||||
"imageSend_quality": "Qualität",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Hoch",
|
||||
"imageSend_packetsLabel": "Pakete",
|
||||
"imageSend_airtimeLabel": "Sendezeit",
|
||||
"imageSend_sizeLabel": "Nutzlast",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{Paket} other{Pakete}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Radioeinstellungen unbekannt",
|
||||
"imageSend_radioUnknownBody": "Verbinden Sie das Gerät, damit die Sendezeit berechnet werden kann.",
|
||||
"imageSend_longSendTitle": "Lange Übertragung",
|
||||
"imageSend_longSendBody": "Dies hält den Kanal für etwa {duration}.",
|
||||
"imageSend_floodNote": "Flutweiterleitung: Jeder Repeater im Bereich leitet jedes Paket weiter, wodurch der Kanal länger als dieser Zeitraum beschäftigt bleibt.",
|
||||
"imageSend_parityTitle": "Wiederherstellungspaket",
|
||||
"imageSend_paritySubtitle": "Ein zusätzliches Paket. Gruppen-Nachrichten werden nicht bestätigt, daher kann der Empfänger das Bild rekonstruieren, falls ein Paket verloren geht.",
|
||||
"imageSend_send": "Senden",
|
||||
"imageSend_cancel": "Stornieren",
|
||||
"imageSend_encodeFailed": "Dieses Bild konnte nicht kodiert werden.",
|
||||
"imageSend_codecDownloading": "Das Bildmodell wird noch heruntergeladen.",
|
||||
"imageSend_codecUnavailable": "Bildversand ist auf diesem Gerät nicht verfügbar.",
|
||||
"imageSend_codecDisabled": "Bildnachrichten sind in den Einstellungen deaktiviert.",
|
||||
"imageSend_deviceUnsupported": "Dieses Radio kann keine Bildpakete senden. Verbinden Sie ein Gerät mit der Companion-Firmware-Version 13 oder neuer.",
|
||||
"imageSend_directMessagesUnsupported": "Bilder werden als Gruppendaten übertragen, daher können sie nur an einen Kanal gesendet werden – nicht per Direktnachricht.",
|
||||
"imageSend_tooLarge": "Dieses Bild wurde in mehr Pakete kodiert, als das Mesh-Format zulässt.",
|
||||
"imageSend_sentConfirmation": "Bild gesendet als {count} {count, plural, =1{Paket} other{Pakete}}.",
|
||||
"imageSend_sendFailed": "Das Bild konnte nicht gesendet werden: {error}",
|
||||
"imageSend_sendingProgress": "Bildversand — Paket {sent} von {total}",
|
||||
"receivedImage_senderPrefix": "Kritisch: Diese Platzhalter {prefix} beibehalten.",
|
||||
"receivedImage_incoming": "{received} von {total} Paketen",
|
||||
"receivedImage_queued": "Warten auf die Entschlüsselung",
|
||||
"receivedImage_tapToDecode": "Tippen Sie, um zu entschlüsseln",
|
||||
"receivedImage_decoding": "Rekonstruktion… ca. 1 s",
|
||||
"receivedImage_incomplete": "Bild unvollständig — {received} von {total} Paketen eingetroffen",
|
||||
"receivedImage_corrupt": "Das Bild konnte nicht rekonstruiert werden.",
|
||||
"receivedImage_decoderMissing": "Bild empfangen — Bilddekodierung fehlgeschlagen",
|
||||
"receivedImage_evicted": "Bild nicht mehr gespeichert",
|
||||
"receivedImage_retry": "Versuchen Sie es erneut",
|
||||
"receivedImage_decodeAgain": "Entschlüsseln Sie es erneut",
|
||||
"receivedImage_openSettings": "Einrichten",
|
||||
"receivedImage_tapToProcess": "Klicken Sie, um fortzufahren",
|
||||
"receivedImage_awaiting": "{bytes} Bytes · {packets} {packets, plural, =1{Paket} other{Pakete}}",
|
||||
"imageSend_secondsValue": "{seconds} s",
|
||||
"imageSend_minutesSecondsValue": "{minutes} Min {seconds} Sek"
|
||||
}
|
||||
|
||||
@@ -158,6 +158,25 @@
|
||||
"settings_radioSettings": "Radio Settings",
|
||||
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
|
||||
"settings_radioSettingsUpdated": "Radio settings updated",
|
||||
"settings_regionSettings": "Regions",
|
||||
"settings_regionSettingsSubtitle": "Manage stored regions",
|
||||
"settings_regionManagement_screenTitle": "Region Management",
|
||||
"settings_regionNameHint": "Enter region name",
|
||||
"settings_regionAddRegion": "Add region",
|
||||
"settings_regionFetchRegions": "Fetch regions from repeaters",
|
||||
"settings_regionFetchRegionsFail": "No regions were found",
|
||||
"settings_regionFetchRegionsAlreadyExists": "This region has already been added",
|
||||
"settings_regionName": "Region Name",
|
||||
"settings_regionDeleted": "Region deleted",
|
||||
"settings_deleteRegion": "Delete Region",
|
||||
"settings_deleteRegionConfirm": "Remove \"{region}\" from region list?",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_location": "Location",
|
||||
"settings_locationSubtitle": "GPS coordinates",
|
||||
"settings_locationUpdated": "Location and GPS settings updated",
|
||||
@@ -187,6 +206,8 @@
|
||||
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
|
||||
"settings_advertLocation": "Advert Location",
|
||||
"settings_advertLocationSubtitle": "Include location in advert.",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Auto Zero-Hop Advert On GPS Update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "When GPS location changes, send a zero-hop advert (requires Advert Location).",
|
||||
"settings_multiAck": "Multi-ACKs",
|
||||
"settings_telemetryModeUpdated": "Telemetry mode updated",
|
||||
"settings_actions": "Actions",
|
||||
@@ -227,6 +248,8 @@
|
||||
"settings_infoPublicKey": "Public Key",
|
||||
"settings_infoContactsCount": "Contacts Count",
|
||||
"settings_infoChannelCount": "Channel Count",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"settings_presets": "Presets",
|
||||
"settings_frequency": "Frequency (MHz)",
|
||||
"settings_frequencyHelper": "300.0 - 2500.0",
|
||||
@@ -271,6 +294,10 @@
|
||||
"appSettings_languageBg": "Български",
|
||||
"appSettings_languageRu": "Русский",
|
||||
"appSettings_languageUk": "Українська",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bytes",
|
||||
"appSettings_enableMessageTracing": "Enable Message Tracing",
|
||||
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
|
||||
"appSettings_notifications": "Notifications",
|
||||
@@ -318,6 +345,7 @@
|
||||
"appSettings_batteryNmc": "18650 NMC (3.0-4.2V)",
|
||||
"appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)",
|
||||
"appSettings_batteryLipo": "LiPo (3.0-4.2V)",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
|
||||
"appSettings_mapDisplay": "Map Display",
|
||||
"appSettings_showRepeaters": "Show Repeaters",
|
||||
"appSettings_showRepeatersSubtitle": "Display repeater nodes on the map",
|
||||
@@ -342,6 +370,19 @@
|
||||
"appSettings_last6Hours": "Last 6 hours",
|
||||
"appSettings_last24Hours": "Last 24 hours",
|
||||
"appSettings_lastWeek": "Last week",
|
||||
"appSettings_rasterTileSource": "Raster Tile Source",
|
||||
"appSettings_stadiaEndpoint": "Stadia Endpoint",
|
||||
"appSettings_stadiaApiKey": "Stadia API Key",
|
||||
"appSettings_stadiaApiKeyRequired": "Required for Stadia Maps usage",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configured: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Enter your Stadia Maps API key. This app uses it for raster tile requests.",
|
||||
"appSettings_offlineMapCache": "Offline Map Cache",
|
||||
"appSettings_unitsTitle": "Units",
|
||||
"appSettings_unitsMetric": "Metric (m / km)",
|
||||
@@ -635,6 +676,18 @@
|
||||
"channels_scanQrCodeComingSoon": "Coming soon",
|
||||
"channels_enterHashtag": "Enter hashtag",
|
||||
"channels_hashtagHint": "e.g. #team",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"channels_regionNotSet": "Region: none",
|
||||
"channels_regionSelect_Title": "Select a region",
|
||||
"channels_clearRegion": "Clear region",
|
||||
"chat_noMessages": "No messages yet",
|
||||
"chat_sendMessage": "Send message",
|
||||
"chat_sendMessageTo": "Send a message to {contactName}",
|
||||
@@ -688,6 +741,12 @@
|
||||
}
|
||||
},
|
||||
"chat_sendGif": "Send GIF",
|
||||
"chat_sendImage": "Send image",
|
||||
"chat_imagePickFailed": "Couldn't open that image",
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"chat_receivedGif": "Received a GIF",
|
||||
"chat_reply": "Reply",
|
||||
"chat_addReaction": "Add Reaction",
|
||||
"chat_me": "Me",
|
||||
@@ -1060,6 +1119,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1377,7 +1486,24 @@
|
||||
"repeater_advancedSettings": "Advanced",
|
||||
"repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators",
|
||||
"repeater_pathHashMode": "Path hash mode",
|
||||
"repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.",
|
||||
"repeater_pathHashModeHelper": "Bytes used to encode this repeater's ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.",
|
||||
"repeater_keySettings": "Change Identity Keys",
|
||||
"repeater_keySettingsSubtitle": "Change the public/private keypair",
|
||||
"repeater_prvKey": "Private key",
|
||||
"repeater_prvKeyHelper": "A new private key for the repeater, a 128-character hex string.",
|
||||
"repeater_generatePrvKey": "Generate a random keypair",
|
||||
"repeater_stopGeneratingPrvKey": "Interrupt search for keypair",
|
||||
"repeater_pubKey": "Public key",
|
||||
"repeater_pubKeyHelper": "This is the public key that goes with the generated private key. You can't set this directly.",
|
||||
"repeater_pubKeyPrefix": "Desired prefix",
|
||||
"repeater_pubKeyPrefixHelper": "Find a public key that starts with these hex digits. Expected tries needed: {tries}.",
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeater_txDelay": "Flood TX delay",
|
||||
"repeater_txDelayHelper": "Retransmit spacing for flood traffic, as a multiplier of the packet's airtime (0-2, default 0.5). Higher = fewer collisions but slower delivery.",
|
||||
"repeater_directTxDelay": "Direct TX delay",
|
||||
@@ -1558,7 +1684,7 @@
|
||||
"repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).",
|
||||
"repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.",
|
||||
"repeater_cliHelpSetDutyCycle": "Sets the maximum allowed transmit duty cycle as a percentage (1-100). Internally adjusts the airtime factor.",
|
||||
"repeater_cliHelpSetPrvKey": "(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.",
|
||||
"repeater_cliHelpSetPrvKey": "Replaces the device identity private key. Reboot required to apply. Generates a new public key.",
|
||||
"repeater_cliHelpSetRadioRxGain": "(SX126x only) Toggles boosted RX gain for improved sensitivity at higher current draw.",
|
||||
"repeater_cliHelpSetOwnerInfo": "Sets the owner contact info string included in adverts. Use '|' for newlines.",
|
||||
"repeater_cliHelpSetPathHashMode": "Sets the path-hash mode. 0 = legacy, 1 = standard, 2 = strict. Affects how routing paths are matched.",
|
||||
@@ -2414,6 +2540,17 @@
|
||||
"radioStats_settingsTile": "Radio stats",
|
||||
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
|
||||
"translation_title": "Translation",
|
||||
"imageMessages_enableTitle": "Enable image messages",
|
||||
"imageMessages_enableSubtitle": "Send images over the mesh. Requires a one-time image model download.",
|
||||
"imageMessages_modelSectionTitle": "Image model",
|
||||
"imageMessages_downloadModel": "Download",
|
||||
"imageMessages_cancelDownload": "Cancel",
|
||||
"imageMessages_removeModel": "Remove model",
|
||||
"imageMessages_modelReady": "Ready",
|
||||
"imageMessages_modelNotPublished": "Not published yet — this build cannot download it.",
|
||||
"imageMessages_downloadFailed": "The image model could not be downloaded.",
|
||||
"imageMessages_autoProcessTitle": "Process images automatically",
|
||||
"imageMessages_autoProcessSubtitle": "Reconstruct every image as soon as it arrives. Uses about 2 GB of memory for a second each time; leave off to reconstruct with a tap instead.",
|
||||
"translation_enableTitle": "Enable translation",
|
||||
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
|
||||
"translation_composerTitle": "Translate before sending",
|
||||
@@ -2595,5 +2732,155 @@
|
||||
"pathMap_expandPanel": "Expand panel",
|
||||
"pathMap_noLocation": "No location",
|
||||
"pathMap_followPacket": "Lock view to packet",
|
||||
"pathMap_unfollowPacket": "Unlock view from packet"
|
||||
"pathMap_unfollowPacket": "Unlock view from packet",
|
||||
"imageSend_title": "Send image",
|
||||
"imageSend_cropNote": "Resized to 512 × 512 · aspect ratio not preserved",
|
||||
"imageSend_originalSize": "Original",
|
||||
"imageSend_onAirSize": "On air",
|
||||
"imageSend_quality": "Quality",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "High",
|
||||
"imageSend_packetsLabel": "Packets",
|
||||
"imageSend_airtimeLabel": "Time on air",
|
||||
"imageSend_sizeLabel": "Payload",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{packet} other{packets}}",
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Radio settings unknown",
|
||||
"imageSend_radioUnknownBody": "Connect to a device so the time on air can be calculated.",
|
||||
"imageSend_longSendTitle": "Long transmission",
|
||||
"imageSend_longSendBody": "This will hold the channel for about {duration}.",
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_floodNote": "Flood routing: every repeater in range retransmits each packet, so the channel stays busy longer than this.",
|
||||
"imageSend_parityTitle": "Add recovery packet",
|
||||
"imageSend_paritySubtitle": "One extra packet. Group messages are not acknowledged, so this lets the receiver rebuild the image if a single packet is lost.",
|
||||
"imageSend_send": "Send",
|
||||
"imageSend_cancel": "Cancel",
|
||||
"imageSend_encodeFailed": "This image could not be encoded.",
|
||||
"imageSend_codecDownloading": "The image model is still downloading.",
|
||||
"imageSend_codecUnavailable": "Image sending is not available on this device.",
|
||||
"imageSend_codecDisabled": "Image messages are turned off in settings.",
|
||||
"imageSend_deviceUnsupported": "This radio cannot send image packets. Connect a device running companion firmware 13 or newer.",
|
||||
"imageSend_directMessagesUnsupported": "Images travel as group data, so they can only be sent to a channel — not in a direct message.",
|
||||
"imageSend_tooLarge": "That image encoded to more packets than the mesh format allows.",
|
||||
"imageSend_sentConfirmation": "Image sent as {count} {count, plural, =1{packet} other{packets}}.",
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_sendFailed": "The image could not be sent: {error}",
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_sendingProgress": "Sending image — packet {sent} of {total}",
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"receivedImage_senderPrefix": "Node {prefix}",
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"receivedImage_incoming": "{received} of {total} packets",
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"receivedImage_queued": "Waiting to decode",
|
||||
"receivedImage_tapToDecode": "Tap to decode",
|
||||
"receivedImage_decoding": "Reconstructing… about 1 s",
|
||||
"receivedImage_incomplete": "Image incomplete — {received} of {total} packets arrived",
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"receivedImage_corrupt": "Image could not be reconstructed",
|
||||
"receivedImage_decoderMissing": "Image received — image decoding is off",
|
||||
"receivedImage_evicted": "Image no longer stored",
|
||||
"receivedImage_retry": "Try again",
|
||||
"receivedImage_decodeAgain": "Decode again",
|
||||
"receivedImage_openSettings": "Set up",
|
||||
"receivedImage_tapToProcess": "Tap to process",
|
||||
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{packet} other{packets}}",
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_secondsValue": "{seconds} s",
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s",
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Latitud o longitud inválidos.",
|
||||
"settings_latitude": "Latitud",
|
||||
"settings_longitude": "Longitud",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Anuncio de cero saltos automático al actualizar GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).",
|
||||
"settings_privacyMode": "Modo de privacidad",
|
||||
"settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios",
|
||||
"settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Últimas 6 horas",
|
||||
"appSettings_last24Hours": "Últimas 24 horas",
|
||||
"appSettings_lastWeek": "La semana pasada",
|
||||
"appSettings_rasterTileSource": "Fuente de teselas ráster",
|
||||
"appSettings_stadiaEndpoint": "Punto de acceso de Stadia",
|
||||
"appSettings_stadiaApiKey": "Clave API de Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obligatorio para usar Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.",
|
||||
"appSettings_offlineMapCache": "Caché de mapa sin conexión",
|
||||
"appSettings_noAreaSelected": "No se ha seleccionado ningún área",
|
||||
"appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})",
|
||||
@@ -791,6 +806,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2674,5 +2739,227 @@
|
||||
"pathMap_noLocation": "Sin ubicación",
|
||||
"pathMap_followPacket": "Seguir paquete",
|
||||
"pathMap_unfollowPacket": "Dejar de seguir el paquete",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regiones",
|
||||
"settings_regionSettingsSubtitle": "Gestionar regiones almacenadas",
|
||||
"settings_regionManagement_screenTitle": "Gestión de Regiones",
|
||||
"settings_regionNameHint": "Nombre de la región",
|
||||
"settings_regionAddRegion": "Añadir región",
|
||||
"settings_regionFetchRegions": "Obtener regiones de repetidores",
|
||||
"settings_regionFetchRegionsFail": "No se encontraron regiones",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Esta región ya ha sido agregada",
|
||||
"settings_regionName": "Nombre de la Región",
|
||||
"settings_regionDeleted": "Región eliminada",
|
||||
"settings_deleteRegion": "Eliminar Región",
|
||||
"settings_deleteRegionConfirm": "¿Eliminar \"{region}\" de la lista de regiones?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bytes",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
|
||||
"channels_regionSetTo": "Región: {region}",
|
||||
"channels_regionNotSet": "Región: ninguna",
|
||||
"channels_regionSelect_Title": "Seleccione una región",
|
||||
"channels_clearRegion": "Zona despejada",
|
||||
"chat_sendImage": "Enviar imagen",
|
||||
"chat_imagePickFailed": "No pude abrir esa imagen",
|
||||
"chat_receivedGif": "Recibido un GIF",
|
||||
"repeater_keySettings": "Cambiar Claves de Identidad",
|
||||
"repeater_keySettingsSubtitle": "Cambiar la pareja de claves pública/privada",
|
||||
"repeater_prvKey": "Clave privada",
|
||||
"repeater_prvKeyHelper": "Una nueva clave privada para el repetidor, una cadena hexadecimal de 128 caracteres.",
|
||||
"repeater_generatePrvKey": "Genera un par de claves aleatorias",
|
||||
"repeater_stopGeneratingPrvKey": "Interrumpir la búsqueda de par de claves",
|
||||
"repeater_pubKey": "Clave pública",
|
||||
"repeater_pubKeyHelper": "Esta es la clave pública que corresponde a la clave privada generada. No se puede establecer directamente.",
|
||||
"repeater_pubKeyPrefix": "Prefijo deseado",
|
||||
"repeater_pubKeyPrefixHelper": "Encuentre una clave pública que comience con estos dígitos hexadecimales. Se esperan {tries} intentos.",
|
||||
"imageMessages_enableTitle": "Mensajes de imagen",
|
||||
"imageMessages_enableSubtitle": "Enviar imágenes a través de la malla. Requiere la descarga de un modelo de imagen único.",
|
||||
"imageMessages_modelSectionTitle": "Modelo de imagen",
|
||||
"imageMessages_downloadModel": "Descargar",
|
||||
"imageMessages_cancelDownload": "Cancelar",
|
||||
"imageMessages_removeModel": "Eliminar modelo",
|
||||
"imageMessages_modelReady": "Listo",
|
||||
"imageMessages_modelNotPublished": "Aún no publicado — esta versión no puede descargarlo.",
|
||||
"imageMessages_downloadFailed": "El modelo de imagen no pudo ser descargado.",
|
||||
"imageMessages_autoProcessTitle": "Procesar imágenes automáticamente",
|
||||
"imageMessages_autoProcessSubtitle": "Reconstruya cada imagen tan pronto como llegue. Utiliza aproximadamente 2 GB de memoria durante un segundo cada vez; desactívelo para reconstruir con un toque.",
|
||||
"imageSend_title": "Enviar imagen",
|
||||
"imageSend_cropNote": "Redimensionado a 512 × 512 · la relación de aspecto no se conservó",
|
||||
"imageSend_originalSize": "Texto original",
|
||||
"imageSend_onAirSize": "En emisión",
|
||||
"imageSend_quality": "Calidad",
|
||||
"imageSend_qualityStandard": "Estándar",
|
||||
"imageSend_qualityHigh": "Alto",
|
||||
"imageSend_packetsLabel": "Paquetes",
|
||||
"imageSend_airtimeLabel": "Tiempo en pantalla",
|
||||
"imageSend_sizeLabel": "Carga útil",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{paquete} other{paquetes}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Configuración de radio desconocida",
|
||||
"imageSend_radioUnknownBody": "Conéctese a un dispositivo para que se pueda calcular la hora de transmisión.",
|
||||
"imageSend_longSendTitle": "Larga transmisión",
|
||||
"imageSend_longSendBody": "Esto mantendrá el canal durante aproximadamente {duration}.",
|
||||
"imageSend_floodNote": "Enrutamiento de inundación: cada repetidor dentro del rango retransmite cada paquete, por lo que el canal permanece ocupado durante más tiempo que esto.",
|
||||
"imageSend_parityTitle": "Paquete de recuperación",
|
||||
"imageSend_paritySubtitle": "Un paquete adicional. Los mensajes grupales no son reconocidos, por lo que esto permite al receptor reconstruir la imagen si se pierde un solo paquete.",
|
||||
"imageSend_send": "Enviar",
|
||||
"imageSend_cancel": "Cancelar",
|
||||
"imageSend_encodeFailed": "Esta imagen no pudo ser codificada.",
|
||||
"imageSend_codecDownloading": "El modelo de imagen todavía se está descargando.",
|
||||
"imageSend_codecUnavailable": "El envío de imágenes no está disponible en este dispositivo.",
|
||||
"imageSend_codecDisabled": "Los mensajes de imagen están desactivados en la configuración.",
|
||||
"imageSend_deviceUnsupported": "Esta radio no puede enviar paquetes de imagen. Conecte un dispositivo con el firmware Companion versión 13 o superior.",
|
||||
"imageSend_directMessagesUnsupported": "Las imágenes se transmiten como datos grupales, por lo que solo se pueden enviar a un canal, no mediante mensajes directos.",
|
||||
"imageSend_tooLarge": "Esa imagen se codificó en más paquetes de los que permite el formato de malla.",
|
||||
"imageSend_sentConfirmation": "Imagen enviada como {count} {count, plural, =1{paquete} other{paquetes}}.",
|
||||
"imageSend_sendFailed": "La imagen no pudo ser enviada: {error}",
|
||||
"imageSend_sendingProgress": "Envío de imagen — paquete {sent} de {total}",
|
||||
"receivedImage_senderPrefix": "Nodo {prefix}",
|
||||
"receivedImage_incoming": "{received} de {total} paquetes",
|
||||
"receivedImage_queued": "Esperando descifrar",
|
||||
"receivedImage_tapToDecode": "Toque para decodificar",
|
||||
"receivedImage_decoding": "Reconstruyendo… aproximadamente 1 segundo",
|
||||
"receivedImage_incomplete": "Imagen incompleta — {received} de {total} paquetes llegados",
|
||||
"receivedImage_corrupt": "La imagen no pudo ser reconstruida",
|
||||
"receivedImage_decoderMissing": "Imagen recibida — la decodificación de la imagen no funciona",
|
||||
"receivedImage_evicted": "Imagen ya no almacenada",
|
||||
"receivedImage_retry": "Inténtalo de nuevo",
|
||||
"receivedImage_decodeAgain": "Decodificar de nuevo",
|
||||
"receivedImage_openSettings": "Configurar",
|
||||
"receivedImage_tapToProcess": "Toque para procesar",
|
||||
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{paquete} other{paquetes}}",
|
||||
"imageSend_secondsValue": "{seconds} segundos",
|
||||
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Latitude ou longitude invalide.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Annonce zéro saut automatique lors de la mise à jour GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l'annonce).",
|
||||
"settings_privacyMode": "Mode de confidentialité",
|
||||
"settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces",
|
||||
"settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Dernières 6 heures",
|
||||
"appSettings_last24Hours": "Dernières 24 heures",
|
||||
"appSettings_lastWeek": "La semaine dernière",
|
||||
"appSettings_rasterTileSource": "Source de tuiles raster",
|
||||
"appSettings_stadiaEndpoint": "Point de terminaison Stadia",
|
||||
"appSettings_stadiaApiKey": "Clé API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Requis pour l’utilisation de Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.",
|
||||
"appSettings_offlineMapCache": "Cache de carte hors ligne",
|
||||
"appSettings_noAreaSelected": "Aucune zone sélectionnée",
|
||||
"appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})",
|
||||
@@ -791,6 +806,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2674,5 +2739,227 @@
|
||||
"pathMap_noLocation": "Aucune localisation",
|
||||
"pathMap_followPacket": "Verrouiller la vue sur le paquet",
|
||||
"pathMap_unfollowPacket": "Déverrouiller la vue du paquet",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Régions",
|
||||
"settings_regionSettingsSubtitle": "Gérer les régions stockées",
|
||||
"settings_regionManagement_screenTitle": "Gestion de région",
|
||||
"settings_regionNameHint": "Entrer le nom de la région",
|
||||
"settings_regionAddRegion": "Ajouter une région",
|
||||
"settings_regionFetchRegions": "Récupérer les régions à partir des répéteurs",
|
||||
"settings_regionFetchRegionsFail": "Aucune région n'a été trouvée",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Cette région a déjà été ajoutée",
|
||||
"settings_regionName": "Nom de la région",
|
||||
"settings_regionDeleted": "Région supprimée",
|
||||
"settings_deleteRegion": "Supprimer la région",
|
||||
"settings_deleteRegionConfirm": "Supprimez {region} de la liste des régions ?",
|
||||
"settings_infoHardware": "Matériel",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 octet",
|
||||
"repeater_pathHashModeOption1": "1 à 2 octets",
|
||||
"repeater_pathHashModeOption2": "2 à 3 octets",
|
||||
"repeater_pathHashModeOption3": "3 à 4 octets",
|
||||
"appSettings_batteryLipoHv": "Tension élevée LiPo (3,0-4,35 V)",
|
||||
"channels_regionSetTo": "Région : {region}",
|
||||
"channels_regionNotSet": "Région : aucune",
|
||||
"channels_regionSelect_Title": "Choisissez une région",
|
||||
"channels_clearRegion": "Zone dégagée",
|
||||
"chat_sendImage": "Envoyer l'image",
|
||||
"chat_imagePickFailed": "Je n'ai pas pu ouvrir cette image",
|
||||
"chat_receivedGif": "Reçu un GIF",
|
||||
"repeater_keySettings": "Modifier les clés d'identité",
|
||||
"repeater_keySettingsSubtitle": "Modifier la paire de clés publique/privée",
|
||||
"repeater_prvKey": "Clé privée",
|
||||
"repeater_prvKeyHelper": "Une nouvelle clé privée pour le répéteur, une chaîne hexadécimale de 128 caractères.",
|
||||
"repeater_generatePrvKey": "Générez une paire de clés aléatoire",
|
||||
"repeater_stopGeneratingPrvKey": "Arrêter la recherche de la paire de clés",
|
||||
"repeater_pubKey": "Clé publique",
|
||||
"repeater_pubKeyHelper": "Voici la clé publique associée à la clé privée générée. Vous ne pouvez pas la définir directement.",
|
||||
"repeater_pubKeyPrefix": "Préfixe souhaité",
|
||||
"repeater_pubKeyPrefixHelper": "Trouvez une clé publique qui commence par ces chiffres hexadécimaux. Nombre de tentatives nécessaires : {tries}.",
|
||||
"imageMessages_enableTitle": "Messages avec images",
|
||||
"imageMessages_enableSubtitle": "Envoyer des images via le maillage. Nécessite un téléchargement unique du modèle d'image.",
|
||||
"imageMessages_modelSectionTitle": "Modèle d'image",
|
||||
"imageMessages_downloadModel": "Télécharger",
|
||||
"imageMessages_cancelDownload": "Annuler",
|
||||
"imageMessages_removeModel": "Supprimer le modèle",
|
||||
"imageMessages_modelReady": "Prêt",
|
||||
"imageMessages_modelNotPublished": "Non publié — cette version ne peut pas le télécharger.",
|
||||
"imageMessages_downloadFailed": "Le modèle d'image ne pouvait pas être téléchargé.",
|
||||
"imageMessages_autoProcessTitle": "Traiter automatiquement les images",
|
||||
"imageMessages_autoProcessSubtitle": "Reconstruisez chaque image dès son arrivée. Cela utilise environ 2 Go de mémoire pendant une seconde à chaque fois ; désactivez la reconstruction au toucher pour éviter cela.",
|
||||
"imageSend_title": "Envoyer l'image",
|
||||
"imageSend_cropNote": "Redimensionné à 512 × 512 · le rapport d’aspect n’a pas été conservé",
|
||||
"imageSend_originalSize": "Texte original\n\nVeuillez traduire le texte suivant en français :",
|
||||
"imageSend_onAirSize": "En direct",
|
||||
"imageSend_quality": "Qualité",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Haut",
|
||||
"imageSend_packetsLabel": "Paquets",
|
||||
"imageSend_airtimeLabel": "Durée de diffusion",
|
||||
"imageSend_sizeLabel": "Charge utile",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{paquet} other{paquets}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Réglages de la radio inconnus",
|
||||
"imageSend_radioUnknownBody": "Connectez-vous à un appareil afin que l'heure de diffusion puisse être calculée.",
|
||||
"imageSend_longSendTitle": "Longue transmission",
|
||||
"imageSend_longSendBody": "Cela maintiendra la chaîne pendant environ {duration}.",
|
||||
"imageSend_floodNote": "Routage des inondations : chaque répéteur dans la zone retransmet chaque paquet, de sorte que le canal reste occupé plus longtemps que cela.",
|
||||
"imageSend_parityTitle": "Paquet de récupération",
|
||||
"imageSend_paritySubtitle": "Un paquet supplémentaire. Les messages de groupe ne sont pas reconnus, ce qui permet au destinataire de reconstruire l'image si un seul paquet est perdu.",
|
||||
"imageSend_send": "Envoyer",
|
||||
"imageSend_cancel": "Annuler",
|
||||
"imageSend_encodeFailed": "Cette image ne peut pas être encodée.",
|
||||
"imageSend_codecDownloading": "Le modèle d'image est toujours en téléchargement.",
|
||||
"imageSend_codecUnavailable": "L'envoi d'images n'est pas disponible sur cet appareil.",
|
||||
"imageSend_codecDisabled": "Les messages d'images sont désactivés dans les paramètres.",
|
||||
"imageSend_deviceUnsupported": "Cette radio ne peut pas envoyer de paquets d'images. Connectez un appareil exécutant le firmware compagnon version 13 ou ultérieure.",
|
||||
"imageSend_directMessagesUnsupported": "Les images se déplacent en tant que données de groupe, elles ne peuvent donc être envoyées que sur un canal — et non par message direct.",
|
||||
"imageSend_tooLarge": "Cette image a été encodée en plus de paquets que le format de maillage ne permet.",
|
||||
"imageSend_sentConfirmation": "Image envoyée en {count} {count, plural, =1{paquet} other{paquets}}.",
|
||||
"imageSend_sendFailed": "L'image n'a pas pu être envoyée : {error}",
|
||||
"imageSend_sendingProgress": "Envoi de l'image — paquet {sent} sur {total}",
|
||||
"receivedImage_senderPrefix": "Nœud {prefix}",
|
||||
"receivedImage_incoming": "{received} sur {total} paquets",
|
||||
"receivedImage_queued": "En attendant de décoder",
|
||||
"receivedImage_tapToDecode": "Tapez pour décoder",
|
||||
"receivedImage_decoding": "Reconstitution… d’environ 1 s",
|
||||
"receivedImage_incomplete": "Image incomplète — {received} sur {total} paquets reçus",
|
||||
"receivedImage_corrupt": "L'image ne pouvait pas être reconstruite",
|
||||
"receivedImage_decoderMissing": "Image reçue — le décodage de l'image est interrompu",
|
||||
"receivedImage_evicted": "Image non stockée",
|
||||
"receivedImage_retry": "Réessayez",
|
||||
"receivedImage_decodeAgain": "Décoder à nouveau",
|
||||
"receivedImage_openSettings": "Mettre en place",
|
||||
"receivedImage_tapToProcess": "Tapez pour traiter",
|
||||
"receivedImage_awaiting": "{bytes} octets · {packets} {packets, plural, =1{paquet} other{paquets}}",
|
||||
"imageSend_secondsValue": "{seconds} s",
|
||||
"imageSend_minutesSecondsValue": "{minutes} minutes {seconds} secondes"
|
||||
}
|
||||
|
||||
@@ -161,6 +161,8 @@
|
||||
"settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.",
|
||||
"settings_latitude": "Szélesség",
|
||||
"settings_longitude": "Hosszúság",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatikus zero-hop hirdetés GPS-frissítéskor",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).",
|
||||
"settings_contactSettings": "Kapcsolati beállítások",
|
||||
"settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.",
|
||||
"settings_privacyMode": "Adatvédelmi mód",
|
||||
@@ -332,6 +334,19 @@
|
||||
"appSettings_last6Hours": "Utolsó 6 óra",
|
||||
"appSettings_last24Hours": "Az elmúlt 24 óra",
|
||||
"appSettings_lastWeek": "Múlt héten",
|
||||
"appSettings_rasterTileSource": "Raszteres csempeforrás",
|
||||
"appSettings_stadiaEndpoint": "Stadia végpont",
|
||||
"appSettings_stadiaApiKey": "Stadia API-kulcs",
|
||||
"appSettings_stadiaApiKeyRequired": "A Stadia Maps használatához kötelező",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfigurálva: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.",
|
||||
"appSettings_offlineMapCache": "Offline térképgyorsítótár",
|
||||
"appSettings_unitsTitle": "Egységek",
|
||||
"appSettings_unitsMetric": "Metrikus (m/km)",
|
||||
@@ -952,6 +967,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2578,5 +2643,227 @@
|
||||
"pathMap_noLocation": "Nincs hely",
|
||||
"pathMap_followPacket": "Nézet zárolása a csomaghoz",
|
||||
"pathMap_unfollowPacket": "A nézet feloldása a csomagból",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Tájak",
|
||||
"settings_regionSettingsSubtitle": "Kincstani megjelenítette gyakorlatos területeket",
|
||||
"settings_regionManagement_screenTitle": "Regionális Kezelés",
|
||||
"settings_regionNameHint": "Felhasználja a regioni név",
|
||||
"settings_regionAddRegion": "Egyes region",
|
||||
"settings_regionFetchRegions": "Féregelmezők a repeaterekból",
|
||||
"settings_regionFetchRegionsFail": "Nem található egyéb területek",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Ez a terítet nemisértették meg.",
|
||||
"settings_regionName": "Feldúr Neve",
|
||||
"settings_regionDeleted": "Felült region",
|
||||
"settings_deleteRegion": "Felajánlatos Tér",
|
||||
"settings_deleteRegionConfirm": "Kell kiszámítsd a {region}ot a részekre?",
|
||||
"settings_infoHardware": "Hardverő",
|
||||
"settings_infoFirmware": "Firmver",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 szavak",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bajt",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bajt",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
|
||||
"channels_regionSetTo": "Tájékoztatás: A {region} -t\n\nEz a tájékoztatás a magyar nyelvben a következően kell szóva:\n\nTájékoztatás: A {region} -t",
|
||||
"channels_regionNotSet": "Tájékoztetés: nem",
|
||||
"channels_regionSelect_Title": "Válassz egy regionot",
|
||||
"channels_clearRegion": "Kéres kör",
|
||||
"chat_sendImage": "Felhasználj az image",
|
||||
"chat_imagePickFailed": "Nem tudtam ezt a fenti fényre kapni",
|
||||
"chat_receivedGif": "GIF-ot kapott",
|
||||
"repeater_keySettings": "Képzőítsekei jelölt",
|
||||
"repeater_keySettingsSubtitle": "Közelítse a közlekedési/private képlékt",
|
||||
"repeater_prvKey": "Kis házas",
|
||||
"repeater_prvKeyHelper": "Kínai nyelvben:\n\nA repeteróhoz új képes kód, 128 karakteri hex-kód.",
|
||||
"repeater_generatePrvKey": "Egy random képléktár generáld meg",
|
||||
"repeater_stopGeneratingPrvKey": "Kéypair szavakat kérdéset kaphatás",
|
||||
"repeater_pubKey": "Kiváló kiegészítő",
|
||||
"repeater_pubKeyHelper": "Ez a kiváló kód, amely tükrözi a gyártott kiegészítő kódot. Ezet nem lehet megjelenítani direktusan.",
|
||||
"repeater_pubKeyPrefix": "Kiváló prefix",
|
||||
"repeater_pubKeyPrefixHelper": "Kiválasztsa meg a közös kódot, amely az alábbi hex-kifüggőnek előre van. Várható kiválasztások száma: {tries}.",
|
||||
"imageMessages_enableTitle": "Fenti hírüket biztosítani",
|
||||
"imageMessages_enableSubtitle": "Fényokat a hálózatban küldd el. Egyértelmű fénymodell hozzáadásra kell.",
|
||||
"imageMessages_modelSectionTitle": "Image model",
|
||||
"imageMessages_downloadModel": "Hozzáladás",
|
||||
"imageMessages_cancelDownload": "Színviselés",
|
||||
"imageMessages_removeModel": "Kisnövezdés",
|
||||
"imageMessages_modelReady": "Hozzá",
|
||||
"imageMessages_modelNotPublished": "Érkeztetve nem — ez a építési módszert nem lehet lejobbítani.",
|
||||
"imageMessages_downloadFailed": "Életmódelmát nem tudtam hozni.",
|
||||
"imageMessages_autoProcessTitle": "Automatikusan kínaltatja fénybetek",
|
||||
"imageMessages_autoProcessSubtitle": "Minden fenti image-t kiválasztsa meg, amikor érkezik. Minden más esetben 2 GB fényre kell szükséges; megoldja a rekonstruálisi a támogatásra egy klikkel.",
|
||||
"imageSend_title": "Felhasználj az image",
|
||||
"imageSend_cropNote": "512 × 512 órák számára megosztva · képszöget nem biztosítva",
|
||||
"imageSend_originalSize": "Következtetés:",
|
||||
"imageSend_onAirSize": "Erőben",
|
||||
"imageSend_quality": "Hosszú Képesség",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "High: Magas",
|
||||
"imageSend_packetsLabel": "Paketek",
|
||||
"imageSend_airtimeLabel": "Kiválasztás a fent",
|
||||
"imageSend_sizeLabel": "Kártya",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{csomag} other{csomag}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Radiooldalok nem tudom",
|
||||
"imageSend_radioUnknownBody": "Kizárdhoz kapcsolódd a rendszert, hogy a súly az az álló időt lehet elismerni.",
|
||||
"imageSend_longSendTitle": "Uzun hírás",
|
||||
"imageSend_longSendBody": "Ez megbízható a kanal {duration}-n.",
|
||||
"imageSend_floodNote": "Fúró irányozás: Minden repeater a fúró körüléggel megköveti mindkét paketet, ezért a kanal eltartja más a lehetséges időt.",
|
||||
"imageSend_parityTitle": "Korzolás paksa",
|
||||
"imageSend_paritySubtitle": "Egy kibőbbes kapszula. Gruppos hírselek nem érkezik, ezért az empfangó nem kell elismerni a jel, ha egy egyes kapszula kaland, hogy nem teljesen érkezz.",
|
||||
"imageSend_send": "Keresés",
|
||||
"imageSend_cancel": "Színviselés",
|
||||
"imageSend_encodeFailed": "Ez a fényet nem tudtam kódolni.",
|
||||
"imageSend_codecDownloading": "Érdekesz az image model megstálása még van.",
|
||||
"imageSend_codecUnavailable": "Felhasználó észintje nem kapható ezen a rendszert.",
|
||||
"imageSend_codecDisabled": "Image hírletek belsőoldal elkalmáznak a konfigurációban.",
|
||||
"imageSend_deviceUnsupported": "Ez a radio nem tudja verseni színekfájltakat. Habilítd a kompatibilis firmware 13 vagy újabb versionot kapcsált eszközet.",
|
||||
"imageSend_directMessagesUnsupported": "Fényiak gyakorolják grupos adatban, ezért nem lehetek versenek egy hozzáfűzésre, csak egy kanalra.",
|
||||
"imageSend_tooLarge": "A figyelmetes fentet nem kínálhat a körközlettel ismétálás elterjesztésére.",
|
||||
"imageSend_sentConfirmation": "Kép elküldve {count} {count, plural, =1{csomagként} other{csomagként}}.",
|
||||
"imageSend_sendFailed": "Fentami nem tűnik: {error}",
|
||||
"imageSend_sendingProgress": "Felhasználó: Lehet a következőkét kifejezetet – {sent} paket {total}",
|
||||
"receivedImage_senderPrefix": "Node {prefix}",
|
||||
"receivedImage_incoming": "Kulcsfontosságú: Kiválasztja meg a {received} {total} paketot",
|
||||
"receivedImage_queued": "Kódoláshoz küldőzve",
|
||||
"receivedImage_tapToDecode": "Kódoláshoz klikd el",
|
||||
"receivedImage_decoding": "Korlátozás… egy 1 perc után",
|
||||
"receivedImage_incomplete": "Kérem, az {received} {total} paketet kapottak.",
|
||||
"receivedImage_corrupt": "Fény nem tudtam megrealítani",
|
||||
"receivedImage_decoderMissing": "Kezd az image — image decodálás nem kapható",
|
||||
"receivedImage_evicted": "Fény nem tartalmazva",
|
||||
"receivedImage_retry": "Kérem kell próbálni",
|
||||
"receivedImage_decodeAgain": "Kodoldó",
|
||||
"receivedImage_openSettings": "Jelzése",
|
||||
"receivedImage_tapToProcess": "Klikd a feljelenítést",
|
||||
"receivedImage_awaiting": "{bytes} bájt · {packets} {packets, plural, =1{csomag} other{csomag}}",
|
||||
"imageSend_secondsValue": "{seconds} sekünd\n\n(Note: The placeholder {seconds} is kept unchanged as requested.)",
|
||||
"imageSend_minutesSecondsValue": "{minutes} per {seconds} sekünd\n\n(Note: The original text appears to be a placeholder format rather than a complete sentence. The translation reflects the literal structure while preserving the placeholders as requested.)"
|
||||
}
|
||||
|
||||
@@ -240,6 +240,19 @@
|
||||
"appSettings_last6Hours": "Ultimi 6 ore",
|
||||
"appSettings_last24Hours": "Ultime 24 ore",
|
||||
"appSettings_lastWeek": "La settimana scorsa",
|
||||
"appSettings_rasterTileSource": "Sorgente tile raster",
|
||||
"appSettings_stadiaEndpoint": "Endpoint Stadia",
|
||||
"appSettings_stadiaApiKey": "Chiave API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obbligatoria per usare Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurata: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.",
|
||||
"appSettings_offlineMapCache": "Cache Mappa Offline",
|
||||
"appSettings_noAreaSelected": "Nessun'area selezionata",
|
||||
"appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})",
|
||||
@@ -837,6 +850,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1175,7 +1238,7 @@
|
||||
"repeater_advancedSettings": "Avanzato",
|
||||
"repeater_advancedSettingsSubtitle": "Manopole di regolazione per operatori esperti",
|
||||
"repeater_pathHashMode": "Modalità di hashing del percorso",
|
||||
"repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).",
|
||||
"repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.",
|
||||
"repeater_txDelay": "Ritardo a Flood, TX",
|
||||
"repeater_txDelayHelper": "Riassegnare lo spazio tra i pacchetti per gestire il traffico intenso, come un moltiplicatore del tempo di trasmissione (da 0 a 2, valore predefinito 0,5). Un valore più alto significa meno collisioni, ma una trasmissione più lenta.",
|
||||
"repeater_directTxDelay": "Ritardo diretto TX",
|
||||
@@ -1673,7 +1736,6 @@
|
||||
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
|
||||
"settings_locationIntervalSec": "Intervallo GPS (Secondi)",
|
||||
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
|
||||
"contacts_manageRoom": "Gestisci server stanza",
|
||||
"room_management": "Gestione del Server di Camera",
|
||||
"@community_joinConfirmation": {
|
||||
"placeholders": {
|
||||
@@ -1883,7 +1945,6 @@
|
||||
}
|
||||
},
|
||||
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
|
||||
"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.",
|
||||
@@ -2058,52 +2119,7 @@
|
||||
"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 stanza...",
|
||||
"contactsSettings_title": "Impostazioni dei contatti",
|
||||
"settings_contactSettings": "Impostazioni di contatto",
|
||||
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
|
||||
@@ -2207,6 +2223,8 @@
|
||||
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
|
||||
"settings_advertLocation": "Posizione dell'annuncio",
|
||||
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Annuncio zero-hop automatico all'aggiornamento GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell'annuncio).",
|
||||
"settings_privacy": "Impostazioni sulla privacy",
|
||||
"settings_denyAll": "Negare tutto",
|
||||
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
|
||||
@@ -2721,5 +2739,227 @@
|
||||
"pathMap_noLocation": "Nessuna posizione",
|
||||
"pathMap_followPacket": "Blocca la vista sul pacchetto",
|
||||
"pathMap_unfollowPacket": "Sblocca la vista dal pacchetto",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regioni",
|
||||
"settings_regionSettingsSubtitle": "Gestione delle regioni salvate",
|
||||
"settings_regionManagement_screenTitle": "Gestione Regionale",
|
||||
"settings_regionNameHint": "Inserisci il nome della regione",
|
||||
"settings_regionAddRegion": "Aggiungi regione",
|
||||
"settings_regionFetchRegions": "Recupera le regioni dai ripetitori",
|
||||
"settings_regionFetchRegionsFail": "Non sono state trovate regioni",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Questa regione è già stata aggiunta",
|
||||
"settings_regionName": "Nome della Regione",
|
||||
"settings_regionDeleted": "Regione cancellata",
|
||||
"settings_deleteRegion": "Elimina Regione",
|
||||
"settings_deleteRegionConfirm": "Rimuovere \"{region}\" dalla lista delle regioni?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 byte",
|
||||
"repeater_pathHashModeOption2": "2 - 3 byte",
|
||||
"repeater_pathHashModeOption3": "3 - 4 byte",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
|
||||
"channels_regionSetTo": "Regione: {region}",
|
||||
"channels_regionNotSet": "Regione: nessuna",
|
||||
"channels_regionSelect_Title": "Scegli una regione",
|
||||
"channels_clearRegion": "Zona chiara",
|
||||
"chat_sendImage": "Invia immagine",
|
||||
"chat_imagePickFailed": "Non sono riuscito ad aprire quell'immagine",
|
||||
"chat_receivedGif": "Ricevuto un GIF",
|
||||
"repeater_keySettings": "Cambia le Chiavi di Identità",
|
||||
"repeater_keySettingsSubtitle": "Cambia la coppia di chiavi pubblica/privata",
|
||||
"repeater_prvKey": "Chiave privata",
|
||||
"repeater_prvKeyHelper": "Una nuova chiave privata per il ripetitore, una stringa esadecimale di 128 caratteri.",
|
||||
"repeater_generatePrvKey": "Genera una coppia di chiavi casuale",
|
||||
"repeater_stopGeneratingPrvKey": "Interrompi la ricerca della coppia di chiavi",
|
||||
"repeater_pubKey": "Chiave pubblica",
|
||||
"repeater_pubKeyHelper": "Questa è la chiave pubblica che accompagna la chiave privata generata. Non è possibile impostarla direttamente.",
|
||||
"repeater_pubKeyPrefix": "Prefisso desiderato",
|
||||
"repeater_pubKeyPrefixHelper": "Trova una chiave pubblica che inizia con queste cifre esadecimali. Tentativi necessari previsti: {tries}.",
|
||||
"imageMessages_enableTitle": "Messaggi con immagini",
|
||||
"imageMessages_enableSubtitle": "Invia le immagini tramite la mesh. È necessario scaricare il modello dell'immagine una tantum.",
|
||||
"imageMessages_modelSectionTitle": "Modello di immagine",
|
||||
"imageMessages_downloadModel": "Scarica",
|
||||
"imageMessages_cancelDownload": "Annullare",
|
||||
"imageMessages_removeModel": "Rimuovi il modello",
|
||||
"imageMessages_modelReady": "Pronto",
|
||||
"imageMessages_modelNotPublished": "Non pubblicato ancora — questa versione non può scaricarlo.",
|
||||
"imageMessages_downloadFailed": "Il modello di immagine non può essere scaricato.",
|
||||
"imageMessages_autoProcessTitle": "Elabora automaticamente le immagini",
|
||||
"imageMessages_autoProcessSubtitle": "Ricostruisci ogni immagine non appena arriva. Utilizza circa 2 GB di memoria per un secondo ogni volta; disattiva la ricostruzione con un semplice tocco.",
|
||||
"imageSend_title": "Invia immagine",
|
||||
"imageSend_cropNote": "Ridimensionato a 512 × 512 · rapporto d'aspetto non conservato",
|
||||
"imageSend_originalSize": "Traduzione:\n\nOriginal text:\n\nPlease translate the following English text into Italian:\n\nOriginal text:\n\n(No original English text provided)",
|
||||
"imageSend_onAirSize": "In onda",
|
||||
"imageSend_quality": "Qualità",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Alto",
|
||||
"imageSend_packetsLabel": "Pacchetti",
|
||||
"imageSend_airtimeLabel": "Tempo in onda",
|
||||
"imageSend_sizeLabel": "Carico utile",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{pacchetto} other{pacchetti}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Impostazioni radio sconosciute",
|
||||
"imageSend_radioUnknownBody": "Collegati a un dispositivo in modo che si possa calcolare l'orario di trasmissione.",
|
||||
"imageSend_longSendTitle": "Lunga trasmissione",
|
||||
"imageSend_longSendBody": "Questo manterrà il canale per circa {duration}.",
|
||||
"imageSend_floodNote": "Instradamento delle inondazioni: ogni ripetitore nel raggio ritrasmette ogni pacchetto, quindi il canale rimane occupato più a lungo.",
|
||||
"imageSend_parityTitle": "Pacchetto di recupero",
|
||||
"imageSend_paritySubtitle": "Un pacchetto aggiuntivo. I messaggi di gruppo non vengono riconosciuti, quindi questo permette al destinatario di ricostruire l'immagine se un singolo pacchetto viene perso.",
|
||||
"imageSend_send": "Invia",
|
||||
"imageSend_cancel": "Annullare",
|
||||
"imageSend_encodeFailed": "Questa immagine non può essere codificata.",
|
||||
"imageSend_codecDownloading": "Il modello di immagine sta ancora scaricando.",
|
||||
"imageSend_codecUnavailable": "L'invio di immagini non è disponibile su questo dispositivo.",
|
||||
"imageSend_codecDisabled": "I messaggi immagine sono disattivati nelle impostazioni.",
|
||||
"imageSend_deviceUnsupported": "Questa radio non può inviare pacchetti di immagini. Collegare un dispositivo con firmware companion versione 13 o successiva.",
|
||||
"imageSend_directMessagesUnsupported": "Le immagini viaggiano come dati di gruppo, quindi possono essere inviate solo a un canale — non tramite messaggio diretto.",
|
||||
"imageSend_tooLarge": "Quella immagine è stata codificata in un numero di pacchetti superiore a quello consentito dal formato mesh.",
|
||||
"imageSend_sentConfirmation": "Immagine inviata come {count} {count, plural, =1{pacchetto} other{pacchetti}}.",
|
||||
"imageSend_sendFailed": "L'immagine non può essere inviata: {error}",
|
||||
"imageSend_sendingProgress": "Invio immagine — pacchetto {sent} di {total}",
|
||||
"receivedImage_senderPrefix": "Nodo {prefix}",
|
||||
"receivedImage_incoming": "{received} di {total} pacchetti",
|
||||
"receivedImage_queued": "In attesa di decodificare",
|
||||
"receivedImage_tapToDecode": "Tocca per decodificare",
|
||||
"receivedImage_decoding": "Ricostruire… circa 1 s",
|
||||
"receivedImage_incomplete": "Immagine incompleta — {received} su {total} pacchetti ricevuti",
|
||||
"receivedImage_corrupt": "L'immagine non potrebbe essere ricostruita",
|
||||
"receivedImage_decoderMissing": "Immagine ricevuta — decodifica dell'immagine non funzionante",
|
||||
"receivedImage_evicted": "Immagine non più memorizzata",
|
||||
"receivedImage_retry": "Prova ancora",
|
||||
"receivedImage_decodeAgain": "Decodifica di nuovo",
|
||||
"receivedImage_openSettings": "Imposta",
|
||||
"receivedImage_tapToProcess": "Premi per elaborare",
|
||||
"receivedImage_awaiting": "{bytes} byte · {packets} {packets, plural, =1{pacchetto} other{pacchetti}}",
|
||||
"imageSend_secondsValue": "{seconds} secondi",
|
||||
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
|
||||
}
|
||||
|
||||
@@ -28,11 +28,11 @@
|
||||
"common_enable": "有効にする",
|
||||
"common_disable": "無効にする",
|
||||
"common_undo": "元に戻す",
|
||||
"messageStatus_sent": "送信済み",
|
||||
"messageStatus_delivered": "配信済み",
|
||||
"messageStatus_pending": "送信中",
|
||||
"messageStatus_failed": "送信に失敗",
|
||||
"messageStatus_repeated": "重複受信",
|
||||
"messageStatus_sent": "送信",
|
||||
"messageStatus_delivered": "配達",
|
||||
"messageStatus_pending": "送信",
|
||||
"messageStatus_failed": "送信できませんでした",
|
||||
"messageStatus_repeated": "何度も聞いた",
|
||||
"common_autoRefresh": "自動更新",
|
||||
"common_interval": "間隔",
|
||||
"common_reboot": "再起動",
|
||||
@@ -167,6 +167,8 @@
|
||||
"settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。",
|
||||
"settings_latitude": "緯度",
|
||||
"settings_longitude": "経度",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。",
|
||||
"settings_contactSettings": "連絡先設定",
|
||||
"settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定",
|
||||
"settings_privacyMode": "プライバシーモード",
|
||||
@@ -338,6 +340,19 @@
|
||||
"appSettings_last6Hours": "過去6時間",
|
||||
"appSettings_last24Hours": "過去24時間",
|
||||
"appSettings_lastWeek": "過去1週間",
|
||||
"appSettings_rasterTileSource": "ラスタタイルのソース",
|
||||
"appSettings_stadiaEndpoint": "Stadia エンドポイント",
|
||||
"appSettings_stadiaApiKey": "Stadia API キー",
|
||||
"appSettings_stadiaApiKeyRequired": "Stadia Maps の利用に必要です",
|
||||
"appSettings_stadiaApiKeyConfigured": "設定済み: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。",
|
||||
"appSettings_offlineMapCache": "オフライン地図キャッシュ",
|
||||
"appSettings_unitsTitle": "単位",
|
||||
"appSettings_unitsMetric": "メートル法 (m / km)",
|
||||
@@ -970,6 +985,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -2532,13 +2597,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_delivered": "配達",
|
||||
"common_undo": "元に戻す",
|
||||
"messageStatus_pending": "送信",
|
||||
"messageStatus_sent": "送信",
|
||||
"messageStatus_failed": "送信できませんでした",
|
||||
"contacts_moreOptions": "さらに多くの選択肢",
|
||||
"messageStatus_repeated": "何度も聞いた",
|
||||
"contacts_searchOpen": "連絡先を検索する",
|
||||
"contacts_searchClose": "検索を終了",
|
||||
"routing_modeFlood": "フラッド",
|
||||
@@ -2680,5 +2739,227 @@
|
||||
"pathMap_noLocation": "位置情報なし",
|
||||
"pathMap_followPacket": "パケットを追跡",
|
||||
"pathMap_unfollowPacket": "パケットの追跡を解除",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "地域",
|
||||
"settings_regionSettingsSubtitle": "保存された領域の管理",
|
||||
"settings_regionManagement_screenTitle": "地域管理",
|
||||
"settings_regionNameHint": "地域名を入力してください",
|
||||
"settings_regionAddRegion": "地域を追加",
|
||||
"settings_regionFetchRegions": "リピーターからのフェッチ地域",
|
||||
"settings_regionFetchRegionsFail": "地域は見つかりませんでした",
|
||||
"settings_regionFetchRegionsAlreadyExists": "この地域はすでに追加されています。",
|
||||
"settings_regionName": "地域名",
|
||||
"settings_regionDeleted": "地域が削除されました",
|
||||
"settings_deleteRegion": "地域を削除",
|
||||
"settings_deleteRegionConfirm": "{region} を地域リストから削除しますか?",
|
||||
"settings_infoHardware": "ハードウェア",
|
||||
"settings_infoFirmware": "ファームウェア",
|
||||
"repeater_pathHashModeOption0": "0 - 1バイト",
|
||||
"repeater_pathHashModeOption1": "1〜2バイト",
|
||||
"repeater_pathHashModeOption2": "2〜3バイト",
|
||||
"repeater_pathHashModeOption3": "3〜4バイト",
|
||||
"appSettings_batteryLipoHv": "リポバッテリー HV(3.0〜4.35V)",
|
||||
"channels_regionSetTo": "地域:{region}",
|
||||
"channels_regionNotSet": "地域:なし",
|
||||
"channels_regionSelect_Title": "地域を選択する",
|
||||
"channels_clearRegion": "クリアな地域",
|
||||
"chat_sendImage": "画像を送信",
|
||||
"chat_imagePickFailed": "その画像を開けませんでした",
|
||||
"chat_receivedGif": "GIFを受け取りました",
|
||||
"repeater_keySettings": "アイデンティティキーの変更",
|
||||
"repeater_keySettingsSubtitle": "公開鍵/秘密鍵を変更する",
|
||||
"repeater_prvKey": "プライベートキー",
|
||||
"repeater_prvKeyHelper": "リピーター用の新しいプライベートキー。128文字の16進文字列です。",
|
||||
"repeater_generatePrvKey": "ランダムな鍵ペアを生成する",
|
||||
"repeater_stopGeneratingPrvKey": "キーペアの検索を中断",
|
||||
"repeater_pubKey": "公開鍵",
|
||||
"repeater_pubKeyHelper": "これは生成された秘密鍵に対応する公開鍵です。直接設定することはできません。",
|
||||
"repeater_pubKeyPrefix": "希望する接頭辞",
|
||||
"repeater_pubKeyPrefixHelper": "これらの16進数で始まる公開鍵を見つけてください。予想される試行回数: {tries}。",
|
||||
"imageMessages_enableTitle": "画像メッセージを有効にする",
|
||||
"imageMessages_enableSubtitle": "メッシュ経由で画像を送信してください。画像モデルを一度限りのダウンロードが必要です。",
|
||||
"imageMessages_modelSectionTitle": "画像モデル",
|
||||
"imageMessages_downloadModel": "ダウンロード",
|
||||
"imageMessages_cancelDownload": "キャンセル",
|
||||
"imageMessages_removeModel": "モデルを削除",
|
||||
"imageMessages_modelReady": "準備完了",
|
||||
"imageMessages_modelNotPublished": "まだ公開されていません — このビルドではダウンロードできません。",
|
||||
"imageMessages_downloadFailed": "画像モデルがダウンロードできませんでした。",
|
||||
"imageMessages_autoProcessTitle": "画像を自動的に処理する",
|
||||
"imageMessages_autoProcessSubtitle": "画像が届き次第、すぐに再構築してください。1回あたり約2GBのメモリを使用します。タップで再構築を解除できます。",
|
||||
"imageSend_title": "画像を送信",
|
||||
"imageSend_cropNote": "512 × 512にリサイズされました。アスペクト比は保持されませんでした。",
|
||||
"imageSend_originalSize": "原文:\nThe early morning light filtered through the curtains, casting a soft glow on the room.\n\n翻訳:\n早朝の光がカーテンをすり抜け、部屋に柔らかな光を投げかけていた。",
|
||||
"imageSend_onAirSize": "放送中",
|
||||
"imageSend_quality": "品質",
|
||||
"imageSend_qualityStandard": "標準",
|
||||
"imageSend_qualityHigh": "ハイ",
|
||||
"imageSend_packetsLabel": "パケット",
|
||||
"imageSend_airtimeLabel": "放送時間",
|
||||
"imageSend_sizeLabel": "ペイロード",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{パケット} other{パケット}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "ラジオの設定が不明です",
|
||||
"imageSend_radioUnknownBody": "放送時間の計算ができるように、デバイスに接続してください。",
|
||||
"imageSend_longSendTitle": "長距離伝送",
|
||||
"imageSend_longSendBody": "このチャンネルは約 {duration} 間保持されます。",
|
||||
"imageSend_floodNote": "洪水ルーティング:範囲内の各リピーターが各パケットを再送信するため、チャネルはこれよりも長く占有されたままになります。",
|
||||
"imageSend_parityTitle": "回復パケット",
|
||||
"imageSend_paritySubtitle": "追加のパケットが1つあります。グループメッセージは確認されないため、1つのパケットが失われても受信者が画像を再構築できます。",
|
||||
"imageSend_send": "送る",
|
||||
"imageSend_cancel": "キャンセル",
|
||||
"imageSend_encodeFailed": "この画像はエンコードできませんでした。",
|
||||
"imageSend_codecDownloading": "画像モデルはまだダウンロード中です。",
|
||||
"imageSend_codecUnavailable": "このデバイスでは画像送信が利用できません。",
|
||||
"imageSend_codecDisabled": "設定で画像メッセージはオフになっています。",
|
||||
"imageSend_deviceUnsupported": "このラジオは画像パケットを送信できません。コンパニオン ファームウェア 13 以降を実行しているデバイスを接続してください。",
|
||||
"imageSend_directMessagesUnsupported": "画像はグループデータとして送信されるため、ダイレクトメッセージではなくチャンネルに送ることができます。",
|
||||
"imageSend_tooLarge": "その画像は、メッシュ形式が許容するよりも多くのパケットにエンコードされています。",
|
||||
"imageSend_sentConfirmation": "{count} {count, plural, =1{パケット} other{パケット}}として画像を送信しました。",
|
||||
"imageSend_sendFailed": "画像を送信できませんでした:{error}",
|
||||
"imageSend_sendingProgress": "画像送信 — パケット {sent} 件、合計 {total} 件",
|
||||
"receivedImage_senderPrefix": "ノード {prefix}",
|
||||
"receivedImage_incoming": "{total} パケット中 {received} パケット",
|
||||
"receivedImage_queued": "復号を待っている",
|
||||
"receivedImage_tapToDecode": "デコードするにはタップしてください。",
|
||||
"receivedImage_decoding": "再構築… 約1秒",
|
||||
"receivedImage_incomplete": "画像が不完全です — {total} パケット中 {received} パケットが到着しました",
|
||||
"receivedImage_corrupt": "画像を再構成することはできませんでした",
|
||||
"receivedImage_decoderMissing": "画像を受信しました — 画像のデコードが失敗しました",
|
||||
"receivedImage_evicted": "画像はもう保存されていません",
|
||||
"receivedImage_retry": "もう一度試してください",
|
||||
"receivedImage_decodeAgain": "もう一度デコードしてください",
|
||||
"receivedImage_openSettings": "設定する",
|
||||
"receivedImage_tapToProcess": "処理するにはタップしてください",
|
||||
"receivedImage_awaiting": "{bytes} バイト・{packets} {packets, plural, =1{パケット} other{パケット}}",
|
||||
"imageSend_secondsValue": "{seconds}秒",
|
||||
"imageSend_minutesSecondsValue": "{minutes}分 {seconds}秒"
|
||||
}
|
||||
|
||||
@@ -161,6 +161,8 @@
|
||||
"settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.",
|
||||
"settings_latitude": "위도",
|
||||
"settings_longitude": "경도",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).",
|
||||
"settings_contactSettings": "연락처 설정",
|
||||
"settings_contactSettingsSubtitle": "연락처 추가 방식 설정",
|
||||
"settings_privacyMode": "개인 정보 보호 모드",
|
||||
@@ -332,6 +334,19 @@
|
||||
"appSettings_last6Hours": "지난 6시간",
|
||||
"appSettings_last24Hours": "지난 24시간",
|
||||
"appSettings_lastWeek": "지난 주",
|
||||
"appSettings_rasterTileSource": "래스터 타일 소스",
|
||||
"appSettings_stadiaEndpoint": "Stadia 엔드포인트",
|
||||
"appSettings_stadiaApiKey": "Stadia API 키",
|
||||
"appSettings_stadiaApiKeyRequired": "Stadia Maps를 사용하려면 필요합니다",
|
||||
"appSettings_stadiaApiKeyConfigured": "설정됨: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.",
|
||||
"appSettings_offlineMapCache": "오프라인 지도 캐시",
|
||||
"appSettings_unitsTitle": "단위",
|
||||
"appSettings_unitsMetric": "미터법 (m / km)",
|
||||
@@ -952,6 +967,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1293,7 +1358,7 @@
|
||||
"repeater_advancedSettings": "고급",
|
||||
"repeater_advancedSettingsSubtitle": "숙련된 운영자를 위한 조절 노브",
|
||||
"repeater_pathHashMode": "패스 해시 모드",
|
||||
"repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개의 ID, 최대 64개의 홉), 1=2 바이트 (65,000개의 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개의 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.",
|
||||
"repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.",
|
||||
"repeater_txDelay": "플러드 TX 지연",
|
||||
"repeater_txDelayHelper": "홍수 시 교통량에 맞춰 재전송 간격을 설정합니다. 이는 패킷의 전송 시간을 곱한 값 (0-2, 기본값 0.5)으로 설정합니다. 값이 클수록 충돌이 줄어들지만 전송 속도가 느려집니다.",
|
||||
"repeater_directTxDelay": "직접적인 TX 지연",
|
||||
@@ -2430,13 +2495,10 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"messageStatus_pending": "발송",
|
||||
"messageStatus_sent": "발송",
|
||||
"messageStatus_delivered": "배송 완료",
|
||||
"common_undo": "되돌리기",
|
||||
"messageStatus_pending": "전송 중",
|
||||
"messageStatus_sent": "전송됨",
|
||||
"messageStatus_delivered": "전달됨",
|
||||
"common_undo": "되돌리기",
|
||||
"messageStatus_failed": "전송 실패",
|
||||
"messageStatus_repeated": "반복 수신됨",
|
||||
"contacts_searchOpen": "연락처 검색",
|
||||
@@ -2677,5 +2739,227 @@
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "지역",
|
||||
"settings_regionSettingsSubtitle": "저장된 지역 관리",
|
||||
"settings_regionManagement_screenTitle": "지역 관리",
|
||||
"settings_regionNameHint": "지역 이름 입력",
|
||||
"settings_regionAddRegion": "지역 추가",
|
||||
"settings_regionFetchRegions": "리피터에서 지역을 가져옵니다.",
|
||||
"settings_regionFetchRegionsFail": "어떤 지역도 발견되지 않았습니다.",
|
||||
"settings_regionFetchRegionsAlreadyExists": "이 지역은 이미 추가되었습니다.",
|
||||
"settings_regionName": "지역 이름",
|
||||
"settings_regionDeleted": "지역 삭제",
|
||||
"settings_deleteRegion": "지역 삭제",
|
||||
"settings_deleteRegionConfirm": "\"{region}\"을(를) 지역 목록에서 삭제하시겠습니까?",
|
||||
"settings_infoHardware": "하드웨어",
|
||||
"settings_infoFirmware": "펌웨어",
|
||||
"repeater_pathHashModeOption0": "0 - 1 바이트",
|
||||
"repeater_pathHashModeOption1": "1~2 바이트",
|
||||
"repeater_pathHashModeOption2": "2 - 3 바이트",
|
||||
"repeater_pathHashModeOption3": "3 - 4 바이트",
|
||||
"appSettings_batteryLipoHv": "리포 HV (3.0-4.35V)",
|
||||
"channels_regionSetTo": "지역: {region}",
|
||||
"channels_regionNotSet": "지역: 없음",
|
||||
"channels_regionSelect_Title": "지역을 선택하세요",
|
||||
"channels_clearRegion": "명확한 지역",
|
||||
"chat_sendImage": "이미지를 보내세요",
|
||||
"chat_imagePickFailed": "그 이미지를 열 수 없었어요.",
|
||||
"chat_receivedGif": "GIF를 받았습니다",
|
||||
"repeater_keySettings": "신원 키 변경",
|
||||
"repeater_keySettingsSubtitle": "공개/비공개 키 쌍을 변경하세요",
|
||||
"repeater_prvKey": "개인 키",
|
||||
"repeater_prvKeyHelper": "리피터의 새로운 개인 키로, 128자리의 16진수 문자열입니다.",
|
||||
"repeater_generatePrvKey": "임의의 키 쌍을 생성하세요.",
|
||||
"repeater_stopGeneratingPrvKey": "키페어 검색 중단",
|
||||
"repeater_pubKey": "공개 키",
|
||||
"repeater_pubKeyHelper": "이것은 생성된 개인 키와 함께 사용되는 공개 키입니다. 직접 설정할 수 없습니다.",
|
||||
"repeater_pubKeyPrefix": "원하는 접두사",
|
||||
"repeater_pubKeyPrefixHelper": "이러한 16진수로 시작하는 공개 키를 찾으세요. 필요한 시도 횟수: {tries}.",
|
||||
"imageMessages_enableTitle": "이미지 메시지 활성화",
|
||||
"imageMessages_enableSubtitle": "메시를 통해 이미지를 전송하세요. 일회성 이미지 모델 다운로드가 필요합니다.",
|
||||
"imageMessages_modelSectionTitle": "이미지 모델",
|
||||
"imageMessages_downloadModel": "다운로드",
|
||||
"imageMessages_cancelDownload": "취소",
|
||||
"imageMessages_removeModel": "모델 제거",
|
||||
"imageMessages_modelReady": "준비 완료",
|
||||
"imageMessages_modelNotPublished": "아직 출판되지 않았습니다 — 이 빌드로는 다운로드할 수 없습니다.",
|
||||
"imageMessages_downloadFailed": "이미지 모델을 다운로드할 수 없었습니다.",
|
||||
"imageMessages_autoProcessTitle": "자동으로 프로세스 이미지를 처리합니다.",
|
||||
"imageMessages_autoProcessSubtitle": "이미지가 도착하는 즉시 재구성하세요. 매번 2GB의 메모리를 사용하며, 탭으로 재구성을 원할 경우 해제하세요.",
|
||||
"imageSend_title": "이미지를 보내세요",
|
||||
"imageSend_cropNote": "512 × 512로 크기가 조정되었으며, 종횡비는 유지되지 않았습니다.",
|
||||
"imageSend_originalSize": "원문 없음",
|
||||
"imageSend_onAirSize": "방송 중",
|
||||
"imageSend_quality": "품질",
|
||||
"imageSend_qualityStandard": "표준",
|
||||
"imageSend_qualityHigh": "높다",
|
||||
"imageSend_packetsLabel": "패킷",
|
||||
"imageSend_airtimeLabel": "방송 시간",
|
||||
"imageSend_sizeLabel": "페이로드",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{패킷} other{패킷}}",
|
||||
"imageSend_range": "{min}~{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "라디오 설정 정보 없음",
|
||||
"imageSend_radioUnknownBody": "방송 시간을 계산할 수 있도록 기기와 연결하세요.",
|
||||
"imageSend_longSendTitle": "장거리 송신",
|
||||
"imageSend_longSendBody": "이것은 약 {duration} 동안 채널을 유지할 것입니다.",
|
||||
"imageSend_floodNote": "홍수 라우팅: 범위 내의 모든 리피터가 각 패킷을 재전송하므로, 채널이 이보다 더 오래 점유 상태를 유지합니다.",
|
||||
"imageSend_parityTitle": "회복 패킷",
|
||||
"imageSend_paritySubtitle": "추가 한 개의 패킷입니다. 그룹 메시지는 확인되지 않으므로, 하나의 패킷이 손실되었을 경우 수신자가 이미지를 다시 구성할 수 있습니다.",
|
||||
"imageSend_send": "보내다",
|
||||
"imageSend_cancel": "취소",
|
||||
"imageSend_encodeFailed": "이 이미지는 인코딩할 수 없습니다.",
|
||||
"imageSend_codecDownloading": "이미지 모델이 아직 다운로드 중입니다.",
|
||||
"imageSend_codecUnavailable": "이 기기에서는 이미지 전송이 불가능합니다.",
|
||||
"imageSend_codecDisabled": "설정에서 이미지 메시지가 꺼져 있습니다.",
|
||||
"imageSend_deviceUnsupported": "이 라디오는 이미지 패킷을 전송할 수 없습니다. 컴패니언 펌웨어 13 이상을 실행하는 장치를 연결해 주세요.",
|
||||
"imageSend_directMessagesUnsupported": "이미지는 그룹 데이터로 전송되므로, 직접 메시지가 아닌 채널로만 보낼 수 있습니다.",
|
||||
"imageSend_tooLarge": "그 이미지는 메시 형식이 허용하는 것보다 더 많은 패킷으로 인코딩되었습니다.",
|
||||
"imageSend_sentConfirmation": "이미지가 {count} {count, plural, =1{패킷} other{패킷}}로 전송되었습니다.",
|
||||
"imageSend_sendFailed": "이미지를 보낼 수 없습니다: {error}",
|
||||
"imageSend_sendingProgress": "이미지 전송 — {sent}개의 {total} 패킷",
|
||||
"receivedImage_senderPrefix": "노드 {prefix}",
|
||||
"receivedImage_incoming": "{total}개 패킷 중 {received}개",
|
||||
"receivedImage_queued": "해독을 기다리는 중입니다.",
|
||||
"receivedImage_tapToDecode": "해독하려면 탭을 누르세요.",
|
||||
"receivedImage_decoding": "재구성하는… 약 1초 정도",
|
||||
"receivedImage_incomplete": "이미지 불완전 — {total}개 중 {received}개 패킷 수신됨",
|
||||
"receivedImage_corrupt": "이미지는 재구성될 수 없습니다.",
|
||||
"receivedImage_decoderMissing": "수신된 이미지 — 이미지 해독이 되지 않았습니다",
|
||||
"receivedImage_evicted": "이미지는 더 이상 저장되지 않았습니다.",
|
||||
"receivedImage_retry": "다시 시도해 보세요",
|
||||
"receivedImage_decodeAgain": "다시 해독하다",
|
||||
"receivedImage_openSettings": "설정하다",
|
||||
"receivedImage_tapToProcess": "처리하려면 탭을 누르세요.",
|
||||
"receivedImage_awaiting": "{bytes} 바이트 · {packets} {packets, plural, =1{패킷} other{패킷}}",
|
||||
"imageSend_secondsValue": "{seconds} 초",
|
||||
"imageSend_minutesSecondsValue": "{minutes}분 {seconds}초"
|
||||
}
|
||||
|
||||
@@ -784,6 +784,78 @@ abstract class AppLocalizations {
|
||||
/// **'Radio settings updated'**
|
||||
String get settings_radioSettingsUpdated;
|
||||
|
||||
/// No description provided for @settings_regionSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Regions'**
|
||||
String get settings_regionSettings;
|
||||
|
||||
/// No description provided for @settings_regionSettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Manage stored regions'**
|
||||
String get settings_regionSettingsSubtitle;
|
||||
|
||||
/// No description provided for @settings_regionManagement_screenTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region Management'**
|
||||
String get settings_regionManagement_screenTitle;
|
||||
|
||||
/// No description provided for @settings_regionNameHint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter region name'**
|
||||
String get settings_regionNameHint;
|
||||
|
||||
/// No description provided for @settings_regionAddRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add region'**
|
||||
String get settings_regionAddRegion;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegions.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Fetch regions from repeaters'**
|
||||
String get settings_regionFetchRegions;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegionsFail.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'No regions were found'**
|
||||
String get settings_regionFetchRegionsFail;
|
||||
|
||||
/// No description provided for @settings_regionFetchRegionsAlreadyExists.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This region has already been added'**
|
||||
String get settings_regionFetchRegionsAlreadyExists;
|
||||
|
||||
/// No description provided for @settings_regionName.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region Name'**
|
||||
String get settings_regionName;
|
||||
|
||||
/// No description provided for @settings_regionDeleted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region deleted'**
|
||||
String get settings_regionDeleted;
|
||||
|
||||
/// No description provided for @settings_deleteRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Delete Region'**
|
||||
String get settings_deleteRegion;
|
||||
|
||||
/// No description provided for @settings_deleteRegionConfirm.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove \"{region}\" from region list?'**
|
||||
String settings_deleteRegionConfirm(String region);
|
||||
|
||||
/// No description provided for @settings_location.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -958,6 +1030,18 @@ abstract class AppLocalizations {
|
||||
/// **'Include location in advert.'**
|
||||
String get settings_advertLocationSubtitle;
|
||||
|
||||
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdate.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Auto Zero-Hop Advert On GPS Update'**
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate;
|
||||
|
||||
/// No description provided for @settings_autoZeroHopAdvertOnGpsUpdateSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'When GPS location changes, send a zero-hop advert (requires Advert Location).'**
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle;
|
||||
|
||||
/// No description provided for @settings_multiAck.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1156,6 +1240,18 @@ abstract class AppLocalizations {
|
||||
/// **'Channel Count'**
|
||||
String get settings_infoChannelCount;
|
||||
|
||||
/// No description provided for @settings_infoHardware.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Hardware'**
|
||||
String get settings_infoHardware;
|
||||
|
||||
/// No description provided for @settings_infoFirmware.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Firmware'**
|
||||
String get settings_infoFirmware;
|
||||
|
||||
/// No description provided for @settings_presets.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1378,6 +1474,30 @@ abstract class AppLocalizations {
|
||||
/// **'Українська'**
|
||||
String get appSettings_languageUk;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption0.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'0 - 1 byte'**
|
||||
String get repeater_pathHashModeOption0;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption1.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'1 - 2 bytes'**
|
||||
String get repeater_pathHashModeOption1;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption2.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'2 - 3 bytes'**
|
||||
String get repeater_pathHashModeOption2;
|
||||
|
||||
/// No description provided for @repeater_pathHashModeOption3.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'3 - 4 bytes'**
|
||||
String get repeater_pathHashModeOption3;
|
||||
|
||||
/// No description provided for @appSettings_enableMessageTracing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1618,6 +1738,12 @@ abstract class AppLocalizations {
|
||||
/// **'LiPo (3.0-4.2V)'**
|
||||
String get appSettings_batteryLipo;
|
||||
|
||||
/// No description provided for @appSettings_batteryLipoHv.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'LiPo HV (3.0-4.35V)'**
|
||||
String get appSettings_batteryLipoHv;
|
||||
|
||||
/// No description provided for @appSettings_mapDisplay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -1720,6 +1846,42 @@ abstract class AppLocalizations {
|
||||
/// **'Last week'**
|
||||
String get appSettings_lastWeek;
|
||||
|
||||
/// No description provided for @appSettings_rasterTileSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Raster Tile Source'**
|
||||
String get appSettings_rasterTileSource;
|
||||
|
||||
/// No description provided for @appSettings_stadiaEndpoint.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stadia Endpoint'**
|
||||
String get appSettings_stadiaEndpoint;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Stadia API Key'**
|
||||
String get appSettings_stadiaApiKey;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyRequired.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Required for Stadia Maps usage'**
|
||||
String get appSettings_stadiaApiKeyRequired;
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyConfigured.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Configured: {maskedKey}'**
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey);
|
||||
|
||||
/// No description provided for @appSettings_stadiaApiKeyDialogDescription.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enter your Stadia Maps API key. This app uses it for raster tile requests.'**
|
||||
String get appSettings_stadiaApiKeyDialogDescription;
|
||||
|
||||
/// No description provided for @appSettings_offlineMapCache.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2458,6 +2620,30 @@ abstract class AppLocalizations {
|
||||
/// **'e.g. #team'**
|
||||
String get channels_hashtagHint;
|
||||
|
||||
/// No description provided for @channels_regionSetTo.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region: {region}'**
|
||||
String channels_regionSetTo(String region);
|
||||
|
||||
/// No description provided for @channels_regionNotSet.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Region: none'**
|
||||
String get channels_regionNotSet;
|
||||
|
||||
/// No description provided for @channels_regionSelect_Title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Select a region'**
|
||||
String get channels_regionSelect_Title;
|
||||
|
||||
/// No description provided for @channels_clearRegion.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Clear region'**
|
||||
String get channels_clearRegion;
|
||||
|
||||
/// No description provided for @chat_noMessages.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -2548,6 +2734,24 @@ abstract class AppLocalizations {
|
||||
/// **'Send GIF'**
|
||||
String get chat_sendGif;
|
||||
|
||||
/// No description provided for @chat_sendImage.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send image'**
|
||||
String get chat_sendImage;
|
||||
|
||||
/// Shown when picking or reading a photo to send fails
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Couldn\'t open that image'**
|
||||
String get chat_imagePickFailed;
|
||||
|
||||
/// No description provided for @chat_receivedGif.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Received a GIF'**
|
||||
String get chat_receivedGif;
|
||||
|
||||
/// No description provided for @chat_reply.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3700,6 +3904,54 @@ abstract class AppLocalizations {
|
||||
/// **'Failed downloads: {count}'**
|
||||
String mapCache_failedDownloads(int count);
|
||||
|
||||
/// No description provided for @mapCache_cachedTilesLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tiles'**
|
||||
String get mapCache_cachedTilesLabel;
|
||||
|
||||
/// No description provided for @mapCache_cachedTileSummaryLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tile summary'**
|
||||
String get mapCache_cachedTileSummaryLabel;
|
||||
|
||||
/// No description provided for @mapCache_bulkDownloadDisabledForSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Offline bulk downloads are disabled for {source}.'**
|
||||
String mapCache_bulkDownloadDisabledForSource(String source);
|
||||
|
||||
/// No description provided for @mapCache_bulkDownloadDisabledInConfig.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Offline bulk downloads are disabled for {source} in this app configuration.'**
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source);
|
||||
|
||||
/// No description provided for @mapCache_summarySource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Source: {source}'**
|
||||
String mapCache_summarySource(String source);
|
||||
|
||||
/// No description provided for @mapCache_summaryCachedTilesForSource.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached tiles for source: {count}'**
|
||||
String mapCache_summaryCachedTilesForSource(int count);
|
||||
|
||||
/// No description provided for @mapCache_summaryCachedInSelection.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cached in selected area/zoom: {count}'**
|
||||
String mapCache_summaryCachedInSelection(int count);
|
||||
|
||||
/// No description provided for @mapCache_summaryApproxCacheSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Approx cache size: {size}'**
|
||||
String mapCache_summaryApproxCacheSize(String size);
|
||||
|
||||
/// No description provided for @mapCache_boundsLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -4649,9 +4901,69 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @repeater_pathHashModeHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.'**
|
||||
/// **'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.'**
|
||||
String get repeater_pathHashModeHelper;
|
||||
|
||||
/// No description provided for @repeater_keySettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change Identity Keys'**
|
||||
String get repeater_keySettings;
|
||||
|
||||
/// No description provided for @repeater_keySettingsSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Change the public/private keypair'**
|
||||
String get repeater_keySettingsSubtitle;
|
||||
|
||||
/// No description provided for @repeater_prvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Private key'**
|
||||
String get repeater_prvKey;
|
||||
|
||||
/// No description provided for @repeater_prvKeyHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'A new private key for the repeater, a 128-character hex string.'**
|
||||
String get repeater_prvKeyHelper;
|
||||
|
||||
/// No description provided for @repeater_generatePrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Generate a random keypair'**
|
||||
String get repeater_generatePrvKey;
|
||||
|
||||
/// No description provided for @repeater_stopGeneratingPrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Interrupt search for keypair'**
|
||||
String get repeater_stopGeneratingPrvKey;
|
||||
|
||||
/// No description provided for @repeater_pubKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Public key'**
|
||||
String get repeater_pubKey;
|
||||
|
||||
/// No description provided for @repeater_pubKeyHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This is the public key that goes with the generated private key. You can\'t set this directly.'**
|
||||
String get repeater_pubKeyHelper;
|
||||
|
||||
/// No description provided for @repeater_pubKeyPrefix.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Desired prefix'**
|
||||
String get repeater_pubKeyPrefix;
|
||||
|
||||
/// No description provided for @repeater_pubKeyPrefixHelper.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Find a public key that starts with these hex digits. Expected tries needed: {tries}.'**
|
||||
String repeater_pubKeyPrefixHelper(int tries);
|
||||
|
||||
/// No description provided for @repeater_txDelay.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -5423,7 +5735,7 @@ abstract class AppLocalizations {
|
||||
/// No description provided for @repeater_cliHelpSetPrvKey.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.'**
|
||||
/// **'Replaces the device identity private key. Reboot required to apply. Generates a new public key.'**
|
||||
String get repeater_cliHelpSetPrvKey;
|
||||
|
||||
/// No description provided for @repeater_cliHelpSetRadioRxGain.
|
||||
@@ -7468,6 +7780,72 @@ abstract class AppLocalizations {
|
||||
/// **'Translation'**
|
||||
String get translation_title;
|
||||
|
||||
/// No description provided for @imageMessages_enableTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Enable image messages'**
|
||||
String get imageMessages_enableTitle;
|
||||
|
||||
/// No description provided for @imageMessages_enableSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send images over the mesh. Requires a one-time image model download.'**
|
||||
String get imageMessages_enableSubtitle;
|
||||
|
||||
/// No description provided for @imageMessages_modelSectionTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image model'**
|
||||
String get imageMessages_modelSectionTitle;
|
||||
|
||||
/// No description provided for @imageMessages_downloadModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Download'**
|
||||
String get imageMessages_downloadModel;
|
||||
|
||||
/// No description provided for @imageMessages_cancelDownload.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get imageMessages_cancelDownload;
|
||||
|
||||
/// No description provided for @imageMessages_removeModel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Remove model'**
|
||||
String get imageMessages_removeModel;
|
||||
|
||||
/// No description provided for @imageMessages_modelReady.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Ready'**
|
||||
String get imageMessages_modelReady;
|
||||
|
||||
/// No description provided for @imageMessages_modelNotPublished.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Not published yet — this build cannot download it.'**
|
||||
String get imageMessages_modelNotPublished;
|
||||
|
||||
/// No description provided for @imageMessages_downloadFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The image model could not be downloaded.'**
|
||||
String get imageMessages_downloadFailed;
|
||||
|
||||
/// No description provided for @imageMessages_autoProcessTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Process images automatically'**
|
||||
String get imageMessages_autoProcessTitle;
|
||||
|
||||
/// No description provided for @imageMessages_autoProcessSubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reconstruct every image as soon as it arrives. Uses about 2 GB of memory for a second each time; leave off to reconstruct with a tap instead.'**
|
||||
String get imageMessages_autoProcessSubtitle;
|
||||
|
||||
/// No description provided for @translation_enableTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -7935,6 +8313,294 @@ abstract class AppLocalizations {
|
||||
/// In en, this message translates to:
|
||||
/// **'Unlock view from packet'**
|
||||
String get pathMap_unfollowPacket;
|
||||
|
||||
/// No description provided for @imageSend_title.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send image'**
|
||||
String get imageSend_title;
|
||||
|
||||
/// No description provided for @imageSend_cropNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Resized to 512 × 512 · aspect ratio not preserved'**
|
||||
String get imageSend_cropNote;
|
||||
|
||||
/// No description provided for @imageSend_originalSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Original'**
|
||||
String get imageSend_originalSize;
|
||||
|
||||
/// No description provided for @imageSend_onAirSize.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'On air'**
|
||||
String get imageSend_onAirSize;
|
||||
|
||||
/// No description provided for @imageSend_quality.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Quality'**
|
||||
String get imageSend_quality;
|
||||
|
||||
/// No description provided for @imageSend_qualityStandard.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Standard'**
|
||||
String get imageSend_qualityStandard;
|
||||
|
||||
/// No description provided for @imageSend_qualityHigh.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'High'**
|
||||
String get imageSend_qualityHigh;
|
||||
|
||||
/// No description provided for @imageSend_packetsLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Packets'**
|
||||
String get imageSend_packetsLabel;
|
||||
|
||||
/// No description provided for @imageSend_airtimeLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Time on air'**
|
||||
String get imageSend_airtimeLabel;
|
||||
|
||||
/// No description provided for @imageSend_sizeLabel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Payload'**
|
||||
String get imageSend_sizeLabel;
|
||||
|
||||
/// No description provided for @imageSend_packetsCount.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{count} {count, plural, =1{packet} other{packets}}'**
|
||||
String imageSend_packetsCount(int count);
|
||||
|
||||
/// No description provided for @imageSend_range.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{min}–{max}'**
|
||||
String imageSend_range(String min, String max);
|
||||
|
||||
/// No description provided for @imageSend_unknownValue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'—'**
|
||||
String get imageSend_unknownValue;
|
||||
|
||||
/// No description provided for @imageSend_radioUnknownTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Radio settings unknown'**
|
||||
String get imageSend_radioUnknownTitle;
|
||||
|
||||
/// No description provided for @imageSend_radioUnknownBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Connect to a device so the time on air can be calculated.'**
|
||||
String get imageSend_radioUnknownBody;
|
||||
|
||||
/// No description provided for @imageSend_longSendTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Long transmission'**
|
||||
String get imageSend_longSendTitle;
|
||||
|
||||
/// No description provided for @imageSend_longSendBody.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This will hold the channel for about {duration}.'**
|
||||
String imageSend_longSendBody(String duration);
|
||||
|
||||
/// No description provided for @imageSend_floodNote.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Flood routing: every repeater in range retransmits each packet, so the channel stays busy longer than this.'**
|
||||
String get imageSend_floodNote;
|
||||
|
||||
/// No description provided for @imageSend_parityTitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Add recovery packet'**
|
||||
String get imageSend_parityTitle;
|
||||
|
||||
/// No description provided for @imageSend_paritySubtitle.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'One extra packet. Group messages are not acknowledged, so this lets the receiver rebuild the image if a single packet is lost.'**
|
||||
String get imageSend_paritySubtitle;
|
||||
|
||||
/// No description provided for @imageSend_send.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Send'**
|
||||
String get imageSend_send;
|
||||
|
||||
/// No description provided for @imageSend_cancel.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Cancel'**
|
||||
String get imageSend_cancel;
|
||||
|
||||
/// No description provided for @imageSend_encodeFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This image could not be encoded.'**
|
||||
String get imageSend_encodeFailed;
|
||||
|
||||
/// No description provided for @imageSend_codecDownloading.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The image model is still downloading.'**
|
||||
String get imageSend_codecDownloading;
|
||||
|
||||
/// No description provided for @imageSend_codecUnavailable.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image sending is not available on this device.'**
|
||||
String get imageSend_codecUnavailable;
|
||||
|
||||
/// No description provided for @imageSend_codecDisabled.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image messages are turned off in settings.'**
|
||||
String get imageSend_codecDisabled;
|
||||
|
||||
/// No description provided for @imageSend_deviceUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'This radio cannot send image packets. Connect a device running companion firmware 13 or newer.'**
|
||||
String get imageSend_deviceUnsupported;
|
||||
|
||||
/// No description provided for @imageSend_directMessagesUnsupported.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Images travel as group data, so they can only be sent to a channel — not in a direct message.'**
|
||||
String get imageSend_directMessagesUnsupported;
|
||||
|
||||
/// No description provided for @imageSend_tooLarge.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'That image encoded to more packets than the mesh format allows.'**
|
||||
String get imageSend_tooLarge;
|
||||
|
||||
/// No description provided for @imageSend_sentConfirmation.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image sent as {count} {count, plural, =1{packet} other{packets}}.'**
|
||||
String imageSend_sentConfirmation(int count);
|
||||
|
||||
/// No description provided for @imageSend_sendFailed.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'The image could not be sent: {error}'**
|
||||
String imageSend_sendFailed(String error);
|
||||
|
||||
/// No description provided for @imageSend_sendingProgress.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Sending image — packet {sent} of {total}'**
|
||||
String imageSend_sendingProgress(int sent, int total);
|
||||
|
||||
/// No description provided for @receivedImage_senderPrefix.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Node {prefix}'**
|
||||
String receivedImage_senderPrefix(String prefix);
|
||||
|
||||
/// No description provided for @receivedImage_incoming.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{received} of {total} packets'**
|
||||
String receivedImage_incoming(int received, int total);
|
||||
|
||||
/// No description provided for @receivedImage_queued.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Waiting to decode'**
|
||||
String get receivedImage_queued;
|
||||
|
||||
/// No description provided for @receivedImage_tapToDecode.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap to decode'**
|
||||
String get receivedImage_tapToDecode;
|
||||
|
||||
/// No description provided for @receivedImage_decoding.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Reconstructing… about 1 s'**
|
||||
String get receivedImage_decoding;
|
||||
|
||||
/// No description provided for @receivedImage_incomplete.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image incomplete — {received} of {total} packets arrived'**
|
||||
String receivedImage_incomplete(int received, int total);
|
||||
|
||||
/// No description provided for @receivedImage_corrupt.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image could not be reconstructed'**
|
||||
String get receivedImage_corrupt;
|
||||
|
||||
/// No description provided for @receivedImage_decoderMissing.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image received — image decoding is off'**
|
||||
String get receivedImage_decoderMissing;
|
||||
|
||||
/// No description provided for @receivedImage_evicted.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Image no longer stored'**
|
||||
String get receivedImage_evicted;
|
||||
|
||||
/// No description provided for @receivedImage_retry.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Try again'**
|
||||
String get receivedImage_retry;
|
||||
|
||||
/// No description provided for @receivedImage_decodeAgain.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Decode again'**
|
||||
String get receivedImage_decodeAgain;
|
||||
|
||||
/// No description provided for @receivedImage_openSettings.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Set up'**
|
||||
String get receivedImage_openSettings;
|
||||
|
||||
/// No description provided for @receivedImage_tapToProcess.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Tap to process'**
|
||||
String get receivedImage_tapToProcess;
|
||||
|
||||
/// No description provided for @receivedImage_awaiting.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{bytes} bytes · {packets} {packets, plural, =1{packet} other{packets}}'**
|
||||
String receivedImage_awaiting(int bytes, int packets);
|
||||
|
||||
/// No description provided for @imageSend_secondsValue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{seconds} s'**
|
||||
String imageSend_secondsValue(String seconds);
|
||||
|
||||
/// No description provided for @imageSend_minutesSecondsValue.
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'{minutes} m {seconds} s'**
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds);
|
||||
}
|
||||
|
||||
class _AppLocalizationsDelegate
|
||||
|
||||
@@ -367,6 +367,47 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Радио настройките са актуализирани';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Региони';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Управляне на хранилищни региони';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Регионално управление';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Възразна название на региона';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Добавете регион';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Извън рептерите от региони';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Не бъде открити никако региони';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Тата регион е уже добавена';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Регионноименоване';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Регион отстранен';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Удаляне на регион';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Състрясна: отпиште \"$region\" от списка региони?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Местоположение';
|
||||
|
||||
@@ -467,6 +508,14 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включи местоположение в обявата';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Автоматична zero-hop обява при GPS обновяване';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Множество ACK';
|
||||
|
||||
@@ -579,6 +628,13 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Брой канали';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Оборудование';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware =>
|
||||
'Програмно-оборонни системни программи (фirmware)';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Предварителни настройки';
|
||||
|
||||
@@ -694,6 +750,18 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Украински';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 байт';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 байта';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 байта';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 байта';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Разрешаване на проследяване на съобщения';
|
||||
@@ -843,6 +911,9 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo ВЗГ (3,0-4,35 В)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Карта за показване';
|
||||
|
||||
@@ -900,6 +971,28 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Миналата седмица';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Източник на растерни плочки';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Крайна точка на Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'API ключ за Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Задължително за използване на Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Конфигурирано: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Въведете своя API ключ за Stadia Maps. Приложението го използва за заявки за растерни плочки.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
|
||||
|
||||
@@ -1330,6 +1423,20 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'напр. #отбор';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Регион: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Регион: ние';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Выберете регион';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Чиста зона';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Няма съобщения.';
|
||||
|
||||
@@ -1385,6 +1492,15 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Изпрати GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Изпрати изображение';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Не мог да откривам та изображин';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Получил GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Отговори';
|
||||
|
||||
@@ -2037,6 +2153,42 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
return 'Неуспешни изтегляния: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2586,6 +2738,42 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Изменяне на идентичности ключа';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Замени ключовият пар от публични/частни';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Частни ключ';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Нова приватна ключ на повторчето, 128-символовна шестнадатерична строка.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Генериране на случайна ключи';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Прервана поискна на ключова парта';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Публична ключ';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Та е публичната ключ, която се свързува с генериранта приватна ключ. Трябва да не настройте тоа напрямо.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Желата префикс';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Наискате публична ключ, който почева с този шестнадатницил. Ожидавани проби: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Забавяне на Flood TX';
|
||||
|
||||
@@ -4339,6 +4527,44 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Превод';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Абilitiraм изображените сообщения';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Изправате изображения чрез сетката. Треба е одноразовна загрузка модел на изображение.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Изображени модель';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Скачане';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Отмена';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Удалете модель';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Готов';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Еще не опубликовано — тази версия не може да скане.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Модел за изображение не може да бъде скачан.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Автоматически обрабатвате изображения';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Катализиране на всята изображение сразу по пришествие. Използване на около 2 ГБ на втора пъти; откажете от реконструкцията чаке при натискане.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Активирайте превода';
|
||||
|
||||
@@ -4622,4 +4848,202 @@ class AppLocalizationsBg extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Спри проследяването на пакета';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Изпрати изображение';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Масштабиран до 512 × 512 · соотношение сторон не съхранясе';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Късно на използване на език:';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'В ефир';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Качество';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Стандарт';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Высок';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Пакети';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Времето на изкъсване';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Баланса';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакета',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Настройки на радио неизвестни';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Усредете се с устройство, за да се отработа часът на изстъпане.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Долга пресилка';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Тази буде поддержива открит отряд на около $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Руйжинг на превъште: всички ретранслятори в диапазонта превратят всички пакети, тъйа каналят оставря дълше, от този час.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Возобновителен пакет';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Едно дополително пакет. Групни извещания не принимаются, след това разбирателят едно пакетто, какщо е пропущен един пакет.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Отправляй';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Отмена';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed =>
|
||||
'Тази изображение не може да бъде кодирано.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Модел на изображение еще разгрузира.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Изглед отправа не е доступен на този устройство.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Изображенията на извещенията е отключена в настройките.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Та радиота не може да пресылава изображени пакети. Подключите устройство, който работи с компанионски прошивка 13 или новия.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Изображенията се превозводя се как групова информация, следато да се отправят единствено на канал — не в лична вестка.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Таата изображение е кодирано в по-числа пакети, отгоре както формат на сетта разрешава.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакета',
|
||||
one: 'пакет',
|
||||
);
|
||||
return 'Изображението е изпратено като $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Изображение не може да бъде отправено: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Отправка наизображение — пакет $sent от $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Узъдето $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received от $total пакета';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Ожидането за декодиране';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Натискнете за декодиране';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Повторно… за приблизително 1 секунда';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Изображение неполно — $received от $total пакети отправили';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Изображение не може да бъде воссоздано';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Получена изображение — разгадка на изображение е вреjan';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Изображение не е хранитено';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Повтори';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Правилейте заново';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Настройте';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Натискнете за обработка';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'пакета',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$bytes байта · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds секунди';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes мин $seconds сек';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,47 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regionen';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Gespeicherte Regionen verwalten';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regions-Verwaltung';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Regions-Namen eingeben';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Region hinzufügen';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Regionen von Wiederholern abrufen';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Es wurden keine Regionen gefunden.';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Diese Region wurde bereits hinzugefügt';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Regions-Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region entfernt';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Region entfernen';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Region \"$region\" aus der Liste entfernen?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Ort';
|
||||
|
||||
@@ -466,6 +507,14 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Standort in die Ankündigung einschließen.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatische Zero-Hop-Ankündigung bei GPS-Update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Mehrfach-ACKs';
|
||||
|
||||
@@ -575,6 +624,12 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Kanäle';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Voreinstellungen';
|
||||
|
||||
@@ -690,6 +745,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainisch';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 Byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 Bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 Bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 Bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Nachrichtenverfolgung aktivieren';
|
||||
@@ -839,6 +906,9 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0–4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0–4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Kartendarstellung';
|
||||
|
||||
@@ -896,6 +966,28 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Letzte Woche';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Quelle für Rasterkacheln';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-Endpunkt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia-API-Schlüssel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Für die Nutzung von Stadia Maps erforderlich';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfiguriert: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
|
||||
|
||||
@@ -1329,6 +1421,20 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'z.B. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: keine';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Region auswählen';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Region zurücksetzen';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Noch keine Nachrichten.';
|
||||
|
||||
@@ -1385,6 +1491,15 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF senden';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Bild senden';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Konnte dieses Bild nicht öffnen.';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Erhalten eines GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Antworten';
|
||||
|
||||
@@ -2036,6 +2151,42 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
return 'Fehlgeschlagene Downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2581,6 +2732,43 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade – ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Identitätsschlüssel ändern';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Ändern Sie das öffentliche/private Schlüsselpaar';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Privatschlüssel';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Ein neuer privater Schlüssel für den Repeater, eine 128-stellige Hexadezimalzeichenkette.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey =>
|
||||
'Generiere ein zufälliges Schlüsselpaar';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Suchvorgang für Schlüsselpaar unterbrechen';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Öffentlicher Schlüssel';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Dies ist der öffentliche Schlüssel, der zum generierten privaten Schlüssel gehört. Sie können dies nicht direkt festlegen.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Gewünschtes Präfix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Suchen Sie einen öffentlichen Schlüssel, der mit diesen Hexadezimalziffern beginnt. Erwartete Versuche: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Flood-TX-Verzögerung';
|
||||
|
||||
@@ -4347,6 +4535,43 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Übersetzung';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Bildnachrichten aktivieren';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Bilder über das Netzwerk senden. Einmaliges Herunterladen eines Bildmodells erforderlich.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Bildmodell';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Herunterladen';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Stornieren';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Modell entfernen';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Bereit';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Noch nicht veröffentlicht – dieser Build kann es nicht herunterladen.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Das Bildmodell konnte nicht heruntergeladen werden.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Automatisch Bilder verarbeiten';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Rekonstruiere jedes Bild sofort nach dem Eintreffen. Verbraucht etwa 2 GB Arbeitsspeicher für jede zweite Wiederherstellung; deaktiviere die automatische Rekonstruktion mit einem Tippen.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
|
||||
|
||||
@@ -4634,4 +4859,204 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Fixierung aufheben';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Bild senden';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Für 512 × 512 skaliert · Seitenverhältnis nicht erhalten';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Originaltext:\n\nThe quick brown fox jumps over the lazy dog.\n\nÜbersetzung:\n\nDer schnelle braune Fuchs springt über den faulen Hund.';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'On air';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Qualität';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Hoch';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pakete';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Sendezeit';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Nutzlast';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Pakete',
|
||||
one: 'Paket',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Radioeinstellungen unbekannt';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Verbinden Sie das Gerät, damit die Sendezeit berechnet werden kann.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Lange Übertragung';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Dies hält den Kanal für etwa $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Flutweiterleitung: Jeder Repeater im Bereich leitet jedes Paket weiter, wodurch der Kanal länger als dieser Zeitraum beschäftigt bleibt.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Wiederherstellungspaket';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Ein zusätzliches Paket. Gruppen-Nachrichten werden nicht bestätigt, daher kann der Empfänger das Bild rekonstruieren, falls ein Paket verloren geht.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Senden';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Stornieren';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed =>
|
||||
'Dieses Bild konnte nicht kodiert werden.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Das Bildmodell wird noch heruntergeladen.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Bildversand ist auf diesem Gerät nicht verfügbar.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Bildnachrichten sind in den Einstellungen deaktiviert.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Dieses Radio kann keine Bildpakete senden. Verbinden Sie ein Gerät mit der Companion-Firmware-Version 13 oder neuer.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Bilder werden als Gruppendaten übertragen, daher können sie nur an einen Kanal gesendet werden – nicht per Direktnachricht.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Dieses Bild wurde in mehr Pakete kodiert, als das Mesh-Format zulässt.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'Pakete',
|
||||
one: 'Paket',
|
||||
);
|
||||
return 'Bild gesendet als $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Das Bild konnte nicht gesendet werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Bildversand — Paket $sent von $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Kritisch: Diese Platzhalter $prefix beibehalten.';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received von $total Paketen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Warten auf die Entschlüsselung';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tippen Sie, um zu entschlüsseln';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Rekonstruktion… ca. 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Bild unvollständig — $received von $total Paketen eingetroffen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt =>
|
||||
'Das Bild konnte nicht rekonstruiert werden.';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Bild empfangen — Bilddekodierung fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Bild nicht mehr gespeichert';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Versuchen Sie es erneut';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Entschlüsseln Sie es erneut';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Einrichten';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Klicken Sie, um fortzufahren';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'Pakete',
|
||||
one: 'Paket',
|
||||
);
|
||||
return '$bytes Bytes · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes Min $seconds Sek';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -363,6 +363,45 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio settings updated';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Manage stored regions';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Region Management';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter region name';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Add region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Fetch regions from repeaters';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No regions were found';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'This region has already been added';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Region Name';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region deleted';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Delete Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remove \"$region\" from region list?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Location';
|
||||
|
||||
@@ -457,6 +496,14 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Include location in advert.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Auto Zero-Hop Advert On GPS Update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'When GPS location changes, send a zero-hop advert (requires Advert Location).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@@ -564,6 +611,12 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Channel Count';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Presets';
|
||||
|
||||
@@ -679,6 +732,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Enable Message Tracing';
|
||||
|
||||
@@ -823,6 +888,9 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Map Display';
|
||||
|
||||
@@ -880,6 +948,28 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Last week';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Raster Tile Source';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia Endpoint';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API Key';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Required for Stadia Maps usage';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configured: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Enter your Stadia Maps API key. This app uses it for raster tile requests.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Map Cache';
|
||||
|
||||
@@ -1300,6 +1390,20 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'e.g. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: none';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Select a region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Clear region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'No messages yet';
|
||||
|
||||
@@ -1355,6 +1459,15 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Send GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Send image';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Couldn\'t open that image';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Received a GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reply';
|
||||
|
||||
@@ -1998,6 +2111,42 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
return 'Failed downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2534,7 +2683,42 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). v1.13 and older firmware drops multi-byte paths — only raise once your network is on v1.14+.';
|
||||
'Bytes used to encode this repeater\'s ID in flood path/loop-detect tags. 0=1 byte (256 IDs, up to 64 hops), 1=2 bytes (65K IDs, up to 32 hops), 2=3 bytes (16M IDs, up to 21 hops). Firmware before v1.14 always used 1-byte paths; v1.14 and newer can be configured for 2- or 3-byte paths.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Change Identity Keys';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Change the public/private keypair';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Private key';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'A new private key for the repeater, a 128-character hex string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generate a random keypair';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Interrupt search for keypair';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'This is the public key that goes with the generated private key. You can\'t set this directly.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Desired prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Find a public key that starts with these hex digits. Expected tries needed: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Flood TX delay';
|
||||
@@ -3006,7 +3190,7 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetPrvKey =>
|
||||
'(Serial only) Replaces the device identity private key. Reboot required to apply. Generates a new public key.';
|
||||
'Replaces the device identity private key. Reboot required to apply. Generates a new public key.';
|
||||
|
||||
@override
|
||||
String get repeater_cliHelpSetRadioRxGain =>
|
||||
@@ -4268,6 +4452,43 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Translation';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Enable image messages';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Send images over the mesh. Requires a one-time image model download.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Image model';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Download';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Cancel';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Remove model';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Ready';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Not published yet — this build cannot download it.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'The image model could not be downloaded.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Process images automatically';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Reconstruct every image as soon as it arrives. Uses about 2 GB of memory for a second each time; leave off to reconstruct with a tap instead.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Enable translation';
|
||||
|
||||
@@ -4549,4 +4770,201 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Send image';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Resized to 512 × 512 · aspect ratio not preserved';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Original';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'On air';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Quality';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'High';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Packets';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Time on air';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Payload';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'packets',
|
||||
one: 'packet',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Radio settings unknown';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Connect to a device so the time on air can be calculated.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Long transmission';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'This will hold the channel for about $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Flood routing: every repeater in range retransmits each packet, so the channel stays busy longer than this.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Add recovery packet';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'One extra packet. Group messages are not acknowledged, so this lets the receiver rebuild the image if a single packet is lost.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Send';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Cancel';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'This image could not be encoded.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'The image model is still downloading.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Image sending is not available on this device.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Image messages are turned off in settings.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'This radio cannot send image packets. Connect a device running companion firmware 13 or newer.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Images travel as group data, so they can only be sent to a channel — not in a direct message.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'That image encoded to more packets than the mesh format allows.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'packets',
|
||||
one: 'packet',
|
||||
);
|
||||
return 'Image sent as $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'The image could not be sent: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Sending image — packet $sent of $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Node $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received of $total packets';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Waiting to decode';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tap to decode';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Reconstructing… about 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Image incomplete — $received of $total packets arrived';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Image could not be reconstructed';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Image received — image decoding is off';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Image no longer stored';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Try again';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Decode again';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Set up';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Tap to process';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'packets',
|
||||
one: 'packet',
|
||||
);
|
||||
return '$bytes bytes · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes m $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +367,46 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regiones';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Gestionar regiones almacenadas';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Gestión de Regiones';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Nombre de la región';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Añadir región';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Obtener regiones de repetidores';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'No se encontraron regiones';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Esta región ya ha sido agregada';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Nombre de la Región';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Región eliminada';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Eliminar Región';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return '¿Eliminar \"$region\" de la lista de regiones?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Ubicación';
|
||||
|
||||
@@ -464,6 +504,14 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Anuncio de cero saltos automático al actualizar GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Múltiples respuestas de confirmación';
|
||||
|
||||
@@ -575,6 +623,12 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Número de canales';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Preajustes';
|
||||
|
||||
@@ -690,6 +744,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Habilitar seguimiento de mensajes';
|
||||
@@ -837,6 +903,9 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Visualización del mapa';
|
||||
|
||||
@@ -894,6 +963,28 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La semana pasada';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Fuente de teselas ráster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Punto de acceso de Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Clave API de Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obligatorio para usar Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurado: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Caché de mapa sin conexión';
|
||||
|
||||
@@ -1327,6 +1418,20 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ej. #equipo';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Región: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Región: ninguna';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Seleccione una región';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Zona despejada';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aún no hay mensajes';
|
||||
|
||||
@@ -1382,6 +1487,15 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Enviar imagen';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'No pude abrir esa imagen';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Recibido un GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
@@ -2031,6 +2145,42 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
return 'Descargas fallidas: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2581,6 +2731,42 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Cambiar Claves de Identidad';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Cambiar la pareja de claves pública/privada';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Clave privada';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Una nueva clave privada para el repetidor, una cadena hexadecimal de 128 caracteres.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Genera un par de claves aleatorias';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Interrumpir la búsqueda de par de claves';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Clave pública';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Esta es la clave pública que corresponde a la clave privada generada. No se puede establecer directamente.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Prefijo deseado';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Encuentre una clave pública que comience con estos dígitos hexadecimales. Se esperan $tries intentos.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Retraso TX por inundación';
|
||||
|
||||
@@ -4343,6 +4529,44 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Traducción';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Mensajes de imagen';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Enviar imágenes a través de la malla. Requiere la descarga de un modelo de imagen único.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Modelo de imagen';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Descargar';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Cancelar';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Eliminar modelo';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Listo';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Aún no publicado — esta versión no puede descargarlo.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'El modelo de imagen no pudo ser descargado.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Procesar imágenes automáticamente';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Reconstruya cada imagen tan pronto como llegue. Utiliza aproximadamente 2 GB de memoria durante un segundo cada vez; desactívelo para reconstruir con un toque.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Habilitar la traducción';
|
||||
|
||||
@@ -4628,4 +4852,203 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Dejar de seguir el paquete';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Enviar imagen';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Redimensionado a 512 × 512 · la relación de aspecto no se conservó';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Texto original';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'En emisión';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Calidad';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Estándar';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Alto';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Paquetes';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Tiempo en pantalla';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Carga útil';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paquetes',
|
||||
one: 'paquete',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle =>
|
||||
'Configuración de radio desconocida';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Conéctese a un dispositivo para que se pueda calcular la hora de transmisión.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Larga transmisión';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Esto mantendrá el canal durante aproximadamente $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Enrutamiento de inundación: cada repetidor dentro del rango retransmite cada paquete, por lo que el canal permanece ocupado durante más tiempo que esto.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Paquete de recuperación';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Un paquete adicional. Los mensajes grupales no son reconocidos, por lo que esto permite al receptor reconstruir la imagen si se pierde un solo paquete.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Cancelar';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Esta imagen no pudo ser codificada.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'El modelo de imagen todavía se está descargando.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'El envío de imágenes no está disponible en este dispositivo.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Los mensajes de imagen están desactivados en la configuración.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Esta radio no puede enviar paquetes de imagen. Conecte un dispositivo con el firmware Companion versión 13 o superior.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Las imágenes se transmiten como datos grupales, por lo que solo se pueden enviar a un canal, no mediante mensajes directos.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Esa imagen se codificó en más paquetes de los que permite el formato de malla.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paquetes',
|
||||
one: 'paquete',
|
||||
);
|
||||
return 'Imagen enviada como $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'La imagen no pudo ser enviada: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Envío de imagen — paquete $sent de $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Nodo $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received de $total paquetes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Esperando descifrar';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Toque para decodificar';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding =>
|
||||
'Reconstruyendo… aproximadamente 1 segundo';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Imagen incompleta — $received de $total paquetes llegados';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'La imagen no pudo ser reconstruida';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Imagen recibida — la decodificación de la imagen no funciona';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Imagen ya no almacenada';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Inténtalo de nuevo';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Decodificar de nuevo';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Configurar';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Toque para procesar';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'paquetes',
|
||||
one: 'paquete',
|
||||
);
|
||||
return '$bytes bytes · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds segundos';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,47 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Régions';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Gérer les régions stockées';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Gestion de région';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Entrer le nom de la région';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Ajouter une région';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions =>
|
||||
'Récupérer les régions à partir des répéteurs';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Aucune région n\'a été trouvée';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Cette région a déjà été ajoutée';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Nom de la région';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Région supprimée';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Supprimer la région';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Supprimez $region de la liste des régions ?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Emplacement';
|
||||
|
||||
@@ -468,6 +509,14 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Inclure la localisation dans l\'annonce';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Annonce zéro saut automatique lors de la mise à jour GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l\'annonce).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Plusieurs accusés de réception';
|
||||
|
||||
@@ -579,6 +628,12 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Nombre de canaux';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Matériel';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Préréglages';
|
||||
|
||||
@@ -694,6 +749,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainien';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 octet';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 à 2 octets';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 à 3 octets';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 à 4 octets';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Activer le traçage des messages';
|
||||
@@ -841,6 +908,9 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'Tension élevée LiPo (3,0-4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Affichage de la carte';
|
||||
|
||||
@@ -898,6 +968,28 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La semaine dernière';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Source de tuiles raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Point de terminaison Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Clé API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Requis pour l’utilisation de Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configuré : $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Entrez votre clé API Stadia Maps. Cette application l’utilise pour les requêtes de tuiles raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de carte hors ligne';
|
||||
|
||||
@@ -1329,6 +1421,20 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #equipe';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Région : $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Région : aucune';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Choisissez une région';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Zone dégagée';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Aucun message pour le moment';
|
||||
|
||||
@@ -1384,6 +1490,15 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Envoyer un GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Envoyer l\'image';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Je n\'ai pas pu ouvrir cette image';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Reçu un GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Répondre';
|
||||
|
||||
@@ -2038,6 +2153,42 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
return 'Téléchargements échoués : $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2592,6 +2743,42 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Modifier les clés d\'identité';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Modifier la paire de clés publique/privée';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Clé privée';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Une nouvelle clé privée pour le répéteur, une chaîne hexadécimale de 128 caractères.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Générez une paire de clés aléatoire';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Arrêter la recherche de la paire de clés';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Clé publique';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Voici la clé publique associée à la clé privée générée. Vous ne pouvez pas la définir directement.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Préfixe souhaité';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Trouvez une clé publique qui commence par ces chiffres hexadécimaux. Nombre de tentatives nécessaires : $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Délai de transmission en flood';
|
||||
|
||||
@@ -4364,6 +4551,44 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Traduction';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Messages avec images';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Envoyer des images via le maillage. Nécessite un téléchargement unique du modèle d\'image.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Modèle d\'image';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Télécharger';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Annuler';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Supprimer le modèle';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Prêt';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Non publié — cette version ne peut pas le télécharger.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Le modèle d\'image ne pouvait pas être téléchargé.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Traiter automatiquement les images';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Reconstruisez chaque image dès son arrivée. Cela utilise environ 2 Go de mémoire pendant une seconde à chaque fois ; désactivez la reconstruction au toucher pour éviter cela.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activer la traduction';
|
||||
|
||||
@@ -4649,4 +4874,203 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Déverrouiller la vue du paquet';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Envoyer l\'image';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Redimensionné à 512 × 512 · le rapport d’aspect n’a pas été conservé';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Texte original\n\nVeuillez traduire le texte suivant en français :';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'En direct';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Qualité';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Haut';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Paquets';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Durée de diffusion';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Charge utile';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paquets',
|
||||
one: 'paquet',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Réglages de la radio inconnus';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Connectez-vous à un appareil afin que l\'heure de diffusion puisse être calculée.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Longue transmission';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Cela maintiendra la chaîne pendant environ $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Routage des inondations : chaque répéteur dans la zone retransmet chaque paquet, de sorte que le canal reste occupé plus longtemps que cela.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Paquet de récupération';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Un paquet supplémentaire. Les messages de groupe ne sont pas reconnus, ce qui permet au destinataire de reconstruire l\'image si un seul paquet est perdu.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Annuler';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Cette image ne peut pas être encodée.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Le modèle d\'image est toujours en téléchargement.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'L\'envoi d\'images n\'est pas disponible sur cet appareil.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Les messages d\'images sont désactivés dans les paramètres.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Cette radio ne peut pas envoyer de paquets d\'images. Connectez un appareil exécutant le firmware compagnon version 13 ou ultérieure.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Les images se déplacent en tant que données de groupe, elles ne peuvent donc être envoyées que sur un canal — et non par message direct.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Cette image a été encodée en plus de paquets que le format de maillage ne permet.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paquets',
|
||||
one: 'paquet',
|
||||
);
|
||||
return 'Image envoyée en $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'L\'image n\'a pas pu être envoyée : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Envoi de l\'image — paquet $sent sur $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Nœud $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received sur $total paquets';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'En attendant de décoder';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tapez pour décoder';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Reconstitution… d’environ 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Image incomplète — $received sur $total paquets reçus';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt =>
|
||||
'L\'image ne pouvait pas être reconstruite';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Image reçue — le décodage de l\'image est interrompu';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Image non stockée';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Réessayez';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Décoder à nouveau';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Mettre en place';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Tapez pour traiter';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'paquets',
|
||||
one: 'paquet',
|
||||
);
|
||||
return '$bytes octets · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes minutes $seconds secondes';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,46 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Tájak';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Kincstani megjelenítette gyakorlatos területeket';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regionális Kezelés';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Felhasználja a regioni név';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Egyes region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Féregelmezők a repeaterekból';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Nem található egyéb területek';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Ez a terítet nemisértették meg.';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Feldúr Neve';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Felült region';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Felajánlatos Tér';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Kell kiszámítsd a ${region}ot a részekre?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'hely';
|
||||
|
||||
@@ -463,6 +503,14 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'A hirdetésben szerepeljen a helyszín.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatikus zero-hop hirdetés GPS-frissítéskor';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACK';
|
||||
|
||||
@@ -572,6 +620,12 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Csatornaszám';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardverő';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmver';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Előbeállítások';
|
||||
|
||||
@@ -689,6 +743,18 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 szavak';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bajt';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bajt';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Üzenetkövetés engedélyezése';
|
||||
|
||||
@@ -832,6 +898,9 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Térkép megjelenítése';
|
||||
|
||||
@@ -890,6 +959,28 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Múlt héten';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Raszteres csempeforrás';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia végpont';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-kulcs';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'A Stadia Maps használatához kötelező';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfigurálva: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline térképgyorsítótár';
|
||||
|
||||
@@ -1322,6 +1413,20 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'például #csapat';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Tájékoztatás: A $region -t\n\nEz a tájékoztatás a magyar nyelvben a következően kell szóva:\n\nTájékoztatás: A $region -t';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Tájékoztetés: nem';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Válassz egy regionot';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Kéres kör';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Még nincsenek üzenetek';
|
||||
|
||||
@@ -1377,6 +1482,15 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF küldése';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Felhasználj az image';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Nem tudtam ezt a fenti fényre kapni';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'GIF-ot kapott';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Válasz';
|
||||
|
||||
@@ -2029,6 +2143,42 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
return 'Sikertelen letöltések: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2575,6 +2725,42 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat – csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Képzőítsekei jelölt';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Közelítse a közlekedési/private képlékt';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Kis házas';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Kínai nyelvben:\n\nA repeteróhoz új képes kód, 128 karakteri hex-kód.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Egy random képléktár generáld meg';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Kéypair szavakat kérdéset kaphatás';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Kiváló kiegészítő';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Ez a kiváló kód, amely tükrözi a gyártott kiegészítő kódot. Ezet nem lehet megjelenítani direktusan.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Kiváló prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Kiválasztsa meg a közös kódot, amely az alábbi hex-kifüggőnek előre van. Várható kiválasztások száma: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Áradási TX késleltetés';
|
||||
|
||||
@@ -4337,6 +4523,43 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Fordítás';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Fenti hírüket biztosítani';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Fényokat a hálózatban küldd el. Egyértelmű fénymodell hozzáadásra kell.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Image model';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Hozzáladás';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Színviselés';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Kisnövezdés';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Hozzá';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Érkeztetve nem — ez a építési módszert nem lehet lejobbítani.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed => 'Életmódelmát nem tudtam hozni.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Automatikusan kínaltatja fénybetek';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Minden fenti image-t kiválasztsa meg, amikor érkezik. Minden más esetben 2 GB fényre kell szükséges; megoldja a rekonstruálisi a támogatásra egy klikkel.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Fordítás engedélyezése';
|
||||
|
||||
@@ -4620,4 +4843,201 @@ class AppLocalizationsHu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'A nézet feloldása a csomagból';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Felhasználj az image';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'512 × 512 órák számára megosztva · képszöget nem biztosítva';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Következtetés:';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'Erőben';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Hosszú Képesség';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'High: Magas';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Paketek';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Kiválasztás a fent';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Kártya';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'csomag',
|
||||
one: 'csomag',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Radiooldalok nem tudom';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Kizárdhoz kapcsolódd a rendszert, hogy a súly az az álló időt lehet elismerni.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Uzun hírás';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Ez megbízható a kanal $duration-n.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Fúró irányozás: Minden repeater a fúró körüléggel megköveti mindkét paketet, ezért a kanal eltartja más a lehetséges időt.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Korzolás paksa';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Egy kibőbbes kapszula. Gruppos hírselek nem érkezik, ezért az empfangó nem kell elismerni a jel, ha egy egyes kapszula kaland, hogy nem teljesen érkezz.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Keresés';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Színviselés';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Ez a fényet nem tudtam kódolni.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Érdekesz az image model megstálása még van.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Felhasználó észintje nem kapható ezen a rendszert.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Image hírletek belsőoldal elkalmáznak a konfigurációban.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Ez a radio nem tudja verseni színekfájltakat. Habilítd a kompatibilis firmware 13 vagy újabb versionot kapcsált eszközet.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Fényiak gyakorolják grupos adatban, ezért nem lehetek versenek egy hozzáfűzésre, csak egy kanalra.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'A figyelmetes fentet nem kínálhat a körközlettel ismétálás elterjesztésére.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'csomagként',
|
||||
one: 'csomagként',
|
||||
);
|
||||
return 'Kép elküldve $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Fentami nem tűnik: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Felhasználó: Lehet a következőkét kifejezetet – $sent paket $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Node $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'Kulcsfontosságú: Kiválasztja meg a $received $total paketot';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Kódoláshoz küldőzve';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Kódoláshoz klikd el';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Korlátozás… egy 1 perc után';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Kérem, az $received $total paketet kapottak.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Fény nem tudtam megrealítani';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Kezd az image — image decodálás nem kapható';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Fény nem tartalmazva';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Kérem kell próbálni';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Kodoldó';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Jelzése';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Klikd a feljelenítést';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'csomag',
|
||||
one: 'csomag',
|
||||
);
|
||||
return '$bytes bájt · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds sekünd\n\n(Note: The placeholder $seconds is kept unchanged as requested.)';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes per $seconds sekünd\n\n(Note: The original text appears to be a placeholder format rather than a complete sentence. The translation reflects the literal structure while preserving the placeholders as requested.)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,48 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regioni';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Gestione delle regioni salvate';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Gestione Regionale';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Inserisci il nome della regione';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Aggiungi regione';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions =>
|
||||
'Recupera le regioni dai ripetitori';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Non sono state trovate regioni';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Questa regione è già stata aggiunta';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Nome della Regione';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Regione cancellata';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Elimina Regione';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Rimuovere \"$region\" dalla lista delle regioni?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Posizione';
|
||||
|
||||
@@ -468,6 +510,14 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Includi la posizione nell\'annuncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Annuncio zero-hop automatico all\'aggiornamento GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell\'annuncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'ACK multipli';
|
||||
|
||||
@@ -577,6 +627,12 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Numero di canali';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Preset';
|
||||
|
||||
@@ -692,6 +748,18 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraino';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 byte';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Abilita tracciamento messaggi';
|
||||
@@ -840,6 +908,9 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Visualizzazione Mappa';
|
||||
|
||||
@@ -897,6 +968,28 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'La settimana scorsa';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Sorgente tile raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Endpoint Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Chiave API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obbligatoria per usare Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurata: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache Mappa Offline';
|
||||
|
||||
@@ -1328,6 +1421,20 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'es. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Regione: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Regione: nessuna';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Scegli una regione';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Zona chiara';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nessun messaggio ancora';
|
||||
|
||||
@@ -1383,6 +1490,16 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Invia GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Invia immagine';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed =>
|
||||
'Non sono riuscito ad aprire quell\'immagine';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Ricevuto un GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Rispondi';
|
||||
|
||||
@@ -2033,6 +2150,42 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
return 'Download falliti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2581,7 +2734,43 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).';
|
||||
'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Cambia le Chiavi di Identità';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Cambia la coppia di chiavi pubblica/privata';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Chiave privata';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Una nuova chiave privata per il ripetitore, una stringa esadecimale di 128 caratteri.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Genera una coppia di chiavi casuale';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Interrompi la ricerca della coppia di chiavi';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Chiave pubblica';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Questa è la chiave pubblica che accompagna la chiave privata generata. Non è possibile impostarla direttamente.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Prefisso desiderato';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Trova una chiave pubblica che inizia con queste cifre esadecimali. Tentativi necessari previsti: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Ritardo a Flood, TX';
|
||||
@@ -4347,6 +4536,44 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Traduzione';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Messaggi con immagini';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Invia le immagini tramite la mesh. È necessario scaricare il modello dell\'immagine una tantum.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Modello di immagine';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Scarica';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Annullare';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Rimuovi il modello';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Pronto';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Non pubblicato ancora — questa versione non può scaricarlo.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Il modello di immagine non può essere scaricato.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Elabora automaticamente le immagini';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Ricostruisci ogni immagine non appena arriva. Utilizza circa 2 GB di memoria per un secondo ogni volta; disattiva la ricostruzione con un semplice tocco.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Abilitare la traduzione';
|
||||
|
||||
@@ -4633,4 +4860,204 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Sblocca la vista dal pacchetto';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Invia immagine';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Ridimensionato a 512 × 512 · rapporto d\'aspetto non conservato';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Traduzione:\n\nOriginal text:\n\nPlease translate the following English text into Italian:\n\nOriginal text:\n\n(No original English text provided)';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'In onda';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Qualità';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Alto';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pacchetti';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Tempo in onda';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Carico utile';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pacchetti',
|
||||
one: 'pacchetto',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Impostazioni radio sconosciute';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Collegati a un dispositivo in modo che si possa calcolare l\'orario di trasmissione.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Lunga trasmissione';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Questo manterrà il canale per circa $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Instradamento delle inondazioni: ogni ripetitore nel raggio ritrasmette ogni pacchetto, quindi il canale rimane occupato più a lungo.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Pacchetto di recupero';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Un pacchetto aggiuntivo. I messaggi di gruppo non vengono riconosciuti, quindi questo permette al destinatario di ricostruire l\'immagine se un singolo pacchetto viene perso.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Invia';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Annullare';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed =>
|
||||
'Questa immagine non può essere codificata.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Il modello di immagine sta ancora scaricando.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'L\'invio di immagini non è disponibile su questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'I messaggi immagine sono disattivati nelle impostazioni.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Questa radio non può inviare pacchetti di immagini. Collegare un dispositivo con firmware companion versione 13 o successiva.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Le immagini viaggiano come dati di gruppo, quindi possono essere inviate solo a un canale — non tramite messaggio diretto.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Quella immagine è stata codificata in un numero di pacchetti superiore a quello consentito dal formato mesh.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pacchetti',
|
||||
one: 'pacchetto',
|
||||
);
|
||||
return 'Immagine inviata come $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'L\'immagine non può essere inviata: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Invio immagine — pacchetto $sent di $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Nodo $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received di $total pacchetti';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'In attesa di decodificare';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tocca per decodificare';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Ricostruire… circa 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Immagine incompleta — $received su $total pacchetti ricevuti';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt =>
|
||||
'L\'immagine non potrebbe essere ricostruita';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Immagine ricevuta — decodifica dell\'immagine non funzionante';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Immagine non più memorizzata';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Prova ancora';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Decodifica di nuovo';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Imposta';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Premi per elaborare';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'pacchetti',
|
||||
one: 'pacchetto',
|
||||
);
|
||||
return '$bytes byte · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds secondi';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +353,44 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => '地域';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => '保存された領域の管理';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => '地域管理';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => '地域名を入力してください';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => '地域を追加';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'リピーターからのフェッチ地域';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => '地域は見つかりませんでした';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists => 'この地域はすでに追加されています。';
|
||||
|
||||
@override
|
||||
String get settings_regionName => '地域名';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => '地域が削除されました';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => '地域を削除';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return '$region を地域リストから削除しますか?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '場所';
|
||||
|
||||
@@ -443,6 +481,13 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS更新時にゼロホップ広告を自動送信';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'マルチ ACK';
|
||||
|
||||
@@ -548,6 +593,12 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'チャンネル数';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'ハードウェア';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'ファームウェア';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'プリセット';
|
||||
|
||||
@@ -663,6 +714,18 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'ウクライナ語';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1バイト';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1〜2バイト';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2〜3バイト';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3〜4バイト';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'メッセージ追跡を有効にする';
|
||||
|
||||
@@ -792,6 +855,9 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'リポバッテリー HV(3.0〜4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => '地図表示';
|
||||
|
||||
@@ -845,6 +911,27 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '過去1週間';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'ラスタタイルのソース';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia エンドポイント';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API キー';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => 'Stadia Maps の利用に必要です';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '設定済み: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'オフライン地図キャッシュ';
|
||||
|
||||
@@ -1259,6 +1346,20 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '例:#チーム';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return '地域:$region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => '地域:なし';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => '地域を選択する';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'クリアな地域';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'まだメッセージは届いていません';
|
||||
|
||||
@@ -1314,6 +1415,15 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF を送信';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => '画像を送信';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'その画像を開けませんでした';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'GIFを受け取りました';
|
||||
|
||||
@override
|
||||
String get chat_reply => '返信';
|
||||
|
||||
@@ -1946,6 +2056,42 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
return '失敗したダウンロード: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2466,6 +2612,38 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'アイデンティティキーの変更';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle => '公開鍵/秘密鍵を変更する';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'プライベートキー';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper => 'リピーター用の新しいプライベートキー。128文字の16進文字列です。';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'ランダムな鍵ペアを生成する';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'キーペアの検索を中断';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => '公開鍵';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper => 'これは生成された秘密鍵に対応する公開鍵です。直接設定することはできません。';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => '希望する接頭辞';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'これらの16進数で始まる公開鍵を見つけてください。予想される試行回数: $tries。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'フロイド・TXでの遅延';
|
||||
|
||||
@@ -4118,6 +4296,42 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => '翻訳';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => '画像メッセージを有効にする';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'メッシュ経由で画像を送信してください。画像モデルを一度限りのダウンロードが必要です。';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => '画像モデル';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'ダウンロード';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'キャンセル';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'モデルを削除';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => '準備完了';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'まだ公開されていません — このビルドではダウンロードできません。';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed => '画像モデルがダウンロードできませんでした。';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => '画像を自動的に処理する';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'画像が届き次第、すぐに再構築してください。1回あたり約2GBのメモリを使用します。タップで再構築を解除できます。';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '翻訳機能を有効にする';
|
||||
|
||||
@@ -4393,4 +4607,195 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'パケットの追跡を解除';
|
||||
|
||||
@override
|
||||
String get imageSend_title => '画像を送信';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote => '512 × 512にリサイズされました。アスペクト比は保持されませんでした。';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'原文:\nThe early morning light filtered through the curtains, casting a soft glow on the room.\n\n翻訳:\n早朝の光がカーテンをすり抜け、部屋に柔らかな光を投げかけていた。';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => '放送中';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => '品質';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => '標準';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'ハイ';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'パケット';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => '放送時間';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'ペイロード';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'パケット',
|
||||
one: 'パケット',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'ラジオの設定が不明です';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody => '放送時間の計算ができるように、デバイスに接続してください。';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => '長距離伝送';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'このチャンネルは約 $duration 間保持されます。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'洪水ルーティング:範囲内の各リピーターが各パケットを再送信するため、チャネルはこれよりも長く占有されたままになります。';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => '回復パケット';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'追加のパケットが1つあります。グループメッセージは確認されないため、1つのパケットが失われても受信者が画像を再構築できます。';
|
||||
|
||||
@override
|
||||
String get imageSend_send => '送る';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'キャンセル';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'この画像はエンコードできませんでした。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading => '画像モデルはまだダウンロード中です。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable => 'このデバイスでは画像送信が利用できません。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled => '設定で画像メッセージはオフになっています。';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'このラジオは画像パケットを送信できません。コンパニオン ファームウェア 13 以降を実行しているデバイスを接続してください。';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'画像はグループデータとして送信されるため、ダイレクトメッセージではなくチャンネルに送ることができます。';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge => 'その画像は、メッシュ形式が許容するよりも多くのパケットにエンコードされています。';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'パケット',
|
||||
one: 'パケット',
|
||||
);
|
||||
return '$count $_temp0として画像を送信しました。';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return '画像を送信できませんでした:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return '画像送信 — パケット $sent 件、合計 $total 件';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'ノード $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$total パケット中 $received パケット';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => '復号を待っている';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'デコードするにはタップしてください。';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => '再構築… 約1秒';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return '画像が不完全です — $total パケット中 $received パケットが到着しました';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => '画像を再構成することはできませんでした';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing => '画像を受信しました — 画像のデコードが失敗しました';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => '画像はもう保存されていません';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'もう一度試してください';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'もう一度デコードしてください';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => '設定する';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => '処理するにはタップしてください';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'パケット',
|
||||
one: 'パケット',
|
||||
);
|
||||
return '$bytes バイト・$packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds秒';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes分 $seconds秒';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -353,6 +353,44 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => '지역';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => '저장된 지역 관리';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => '지역 관리';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => '지역 이름 입력';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => '지역 추가';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => '리피터에서 지역을 가져옵니다.';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => '어떤 지역도 발견되지 않았습니다.';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists => '이 지역은 이미 추가되었습니다.';
|
||||
|
||||
@override
|
||||
String get settings_regionName => '지역 이름';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => '지역 삭제';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => '지역 삭제';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return '\"$region\"을(를) 지역 목록에서 삭제하시겠습니까?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '위치';
|
||||
|
||||
@@ -444,6 +482,14 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'GPS 업데이트 시 제로 홉 광고 자동 전송';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => '다중 ACK';
|
||||
|
||||
@@ -548,6 +594,12 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => '채널 수';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => '하드웨어';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => '펌웨어';
|
||||
|
||||
@override
|
||||
String get settings_presets => '프리셋';
|
||||
|
||||
@@ -663,6 +715,18 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => '우크라이나어';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 바이트';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1~2 바이트';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 바이트';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 바이트';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화';
|
||||
|
||||
@@ -794,6 +858,9 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => '리튬 폴리머 (3.0-4.2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => '리포 HV (3.0-4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => '지도 표시';
|
||||
|
||||
@@ -847,6 +914,27 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '지난 주';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => '래스터 타일 소스';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia 엔드포인트';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API 키';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => 'Stadia Maps를 사용하려면 필요합니다';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '설정됨: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Stadia Maps API 키를 입력하세요. 이 앱은 래스터 타일 요청에 이 키를 사용합니다.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => '오프라인 지도 캐시';
|
||||
|
||||
@@ -1261,6 +1349,20 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '예: #팀';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return '지역: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => '지역: 없음';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => '지역을 선택하세요';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => '명확한 지역';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => '아직 메시지가 없습니다.';
|
||||
|
||||
@@ -1316,6 +1418,15 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF 보내기';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => '이미지를 보내세요';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => '그 이미지를 열 수 없었어요.';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'GIF를 받았습니다';
|
||||
|
||||
@override
|
||||
String get chat_reply => '답변';
|
||||
|
||||
@@ -1949,6 +2060,42 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
return '실패한 다운로드: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2469,7 +2616,40 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개의 ID, 최대 64개의 홉), 1=2 바이트 (65,000개의 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개의 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.';
|
||||
'이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => '신원 키 변경';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle => '공개/비공개 키 쌍을 변경하세요';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => '개인 키';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper => '리피터의 새로운 개인 키로, 128자리의 16진수 문자열입니다.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => '임의의 키 쌍을 생성하세요.';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => '키페어 검색 중단';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => '공개 키';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'이것은 생성된 개인 키와 함께 사용되는 공개 키입니다. 직접 설정할 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => '원하는 접두사';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return '이러한 16진수로 시작하는 공개 키를 찾으세요. 필요한 시도 횟수: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => '플러드 TX 지연';
|
||||
@@ -4125,6 +4305,42 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => '번역';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => '이미지 메시지 활성화';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'메시를 통해 이미지를 전송하세요. 일회성 이미지 모델 다운로드가 필요합니다.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => '이미지 모델';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => '다운로드';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => '취소';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => '모델 제거';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => '준비 완료';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'아직 출판되지 않았습니다 — 이 빌드로는 다운로드할 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed => '이미지 모델을 다운로드할 수 없었습니다.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => '자동으로 프로세스 이미지를 처리합니다.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'이미지가 도착하는 즉시 재구성하세요. 매번 2GB의 메모리를 사용하며, 탭으로 재구성을 원할 경우 해제하세요.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '번역 기능 활성화';
|
||||
|
||||
@@ -4400,4 +4616,194 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => '패킷 고정 해제';
|
||||
|
||||
@override
|
||||
String get imageSend_title => '이미지를 보내세요';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote => '512 × 512로 크기가 조정되었으며, 종횡비는 유지되지 않았습니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => '원문 없음';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => '방송 중';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => '품질';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => '표준';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => '높다';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => '패킷';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => '방송 시간';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => '페이로드';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '패킷',
|
||||
one: '패킷',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min~$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => '라디오 설정 정보 없음';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody => '방송 시간을 계산할 수 있도록 기기와 연결하세요.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => '장거리 송신';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return '이것은 약 $duration 동안 채널을 유지할 것입니다.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'홍수 라우팅: 범위 내의 모든 리피터가 각 패킷을 재전송하므로, 채널이 이보다 더 오래 점유 상태를 유지합니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => '회복 패킷';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'추가 한 개의 패킷입니다. 그룹 메시지는 확인되지 않으므로, 하나의 패킷이 손실되었을 경우 수신자가 이미지를 다시 구성할 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => '보내다';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => '취소';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => '이 이미지는 인코딩할 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading => '이미지 모델이 아직 다운로드 중입니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable => '이 기기에서는 이미지 전송이 불가능합니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled => '설정에서 이미지 메시지가 꺼져 있습니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'이 라디오는 이미지 패킷을 전송할 수 없습니다. 컴패니언 펌웨어 13 이상을 실행하는 장치를 연결해 주세요.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'이미지는 그룹 데이터로 전송되므로, 직접 메시지가 아닌 채널로만 보낼 수 있습니다.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge => '그 이미지는 메시 형식이 허용하는 것보다 더 많은 패킷으로 인코딩되었습니다.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '패킷',
|
||||
one: '패킷',
|
||||
);
|
||||
return '이미지가 $count $_temp0로 전송되었습니다.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return '이미지를 보낼 수 없습니다: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return '이미지 전송 — $sent개의 $total 패킷';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return '노드 $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$total개 패킷 중 $received개';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => '해독을 기다리는 중입니다.';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => '해독하려면 탭을 누르세요.';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => '재구성하는… 약 1초 정도';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return '이미지 불완전 — $total개 중 $received개 패킷 수신됨';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => '이미지는 재구성될 수 없습니다.';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing => '수신된 이미지 — 이미지 해독이 되지 않았습니다';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => '이미지는 더 이상 저장되지 않았습니다.';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => '다시 시도해 보세요';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => '다시 해독하다';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => '설정하다';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => '처리하려면 탭을 누르세요.';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: '패킷',
|
||||
one: '패킷',
|
||||
);
|
||||
return '$bytes 바이트 · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds 초';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes분 $seconds초';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,47 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regio\'s';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Beheer van opgeslagen regio\'s';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regionale Bestuur';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Regio naam invoeren';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Regio toevoegen';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Haal regio\'s uit herhalers';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Er zijn geen regio\'s gevonden';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Deze regio is al toegevoegd.';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Regio Naam';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Regio verwijderd';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Regio verwijden';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Verwijder \"$region\" uit de regio-lijst?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Locatie';
|
||||
|
||||
@@ -462,6 +503,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Locatie opnemen in advertentie';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatische zero-hop-advertentie bij GPS-update';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Meerdere bevestigingen';
|
||||
|
||||
@@ -571,6 +620,12 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Aantal Kanalen';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Voorgeprogrammeerde instellingen';
|
||||
|
||||
@@ -686,6 +741,18 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Oekraïens';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
|
||||
|
||||
@@ -832,6 +899,9 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Kaartweergave';
|
||||
|
||||
@@ -889,6 +959,28 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Afgelopen week';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Rastertegelbron';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-eindpunt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-sleutel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Vereist voor gebruik van Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Geconfigureerd: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kaartcache';
|
||||
|
||||
@@ -1316,6 +1408,20 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'bijv. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Regio: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Regio: geen';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Kies een regio';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Duidelijke regio';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Nog geen berichten.';
|
||||
|
||||
@@ -1371,6 +1477,15 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'GIF verzenden';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Beeld verzenden';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Kunde die afbeelding niet openen';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Ontvangen van een GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Reageren';
|
||||
|
||||
@@ -2021,6 +2136,42 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
return 'Mislukte downloads: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2562,7 +2713,42 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden – alleen na het activeren van het netwerk.';
|
||||
'Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID\'s, tot 64 hops), 1=2 bytes (65.000 ID\'s, tot 32 hops), 2=3 bytes (16 miljoen ID\'s, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Verander de Identiteitskies';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Wijzig de openbare/private sleutelpaar';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Geheime sleutel';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Een nieuwe private sleutel voor de repeater, een 128-karakter lange hexadecimale string.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Stel een willekeurige sleutelpaar op';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Stop zoek naar sleutelpaar';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Openbaar sleutel';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Dit is de openbare sleutel die past bij de gegenereerde privésleutel. U kunt dit niet direct instellen.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Verlangde voorvoegsel';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Zoek een openbare sleutel die begint met deze hexadecimale cijfers. Verwacht aantal pogingen: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Vertraging bij Flood TX';
|
||||
@@ -4325,6 +4511,43 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Vertaling';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Bijbehorende afbeeldingen activeren';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Beeldjes verzenden via het netwerk. Vereist eenmalig afdaling van een beeldmodel.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Beeldmodel';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Download';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Annuleren';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Model verwijderen';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Klaar';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Nog niet gepubliceerd — deze versie kan dit niet downloaden.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Het beeldmodel kon niet worden gedownload.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Beeldverwerking automatisch';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Reconstrueer elk beeld direct nadat het binnenkomt. Het gebruikt ongeveer 2 GB geheugen voor elke keer; schakel het reconstrueren uit met een tik als je het niet nodig hebt.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Activeer vertaling';
|
||||
|
||||
@@ -4608,4 +4831,202 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Weergave ontgrendelen van pakket';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Beeld verzenden';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Aangepast aan 512 × 512 · het aspectverhouding is niet behouden';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Oorspronkelijk';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'Op de lucht';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Kwaliteit';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standaard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Hoog';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pakketten';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Tijd op de lucht';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Belasting';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pakketten',
|
||||
one: 'pakket',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Radio-instellingen onbekend';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Verbinding maken met een apparaat zodat de uitzendtijd kan worden berekend.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Lange transmissie';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Dit houdt de kanaal ongeveer voor $duration stand.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Overstromingsrouting: elke repeater binnen bereik herzendsends elk pakket opnieuw, waardoor het kanaal langer bezig blijft dan dit.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Herstelpakket';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Een extra pakje. Groepsberichten worden niet bevestigd, waardoor de ontvanger het beeld kan herstellen als een enkel pakket verloren gaat.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Stuur';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Annuleren';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Dit beeld kon niet worden gecodeerd.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Het beeldmodel wordt nog steeds gedownload.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Het verzenden van een afbeelding is niet mogelijk op dit apparaat.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Boodschappen met afbeeldingen zijn uitgeschakeld in de instellingen.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Dit radio kan geen beeldpakketten verzenden. Verbind een apparaat met de bijbehorende firmware versie 13 of nieuwer.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Afbeeldingen worden verzonden als groepsgegevens, dus ze kunnen alleen naar een kanaal worden gestuurd — niet via een directe boodschap.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Die afbeelding werd gecodeerd in meer pakketten dan het mesh-formaat toelaat.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pakketten',
|
||||
one: 'pakket',
|
||||
);
|
||||
return 'Afbeelding verzonden als $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Het beeld kon niet worden verzonden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Beeld verzenden — pakket $sent van $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Knipper $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received van de $total pakketten';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Wachten op het ontcijferen';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tik om te decoderen';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Herstellen… ongeveer 1 seconde';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Afbeelding onvolledig — $received van $total pakketten ontvangen';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt =>
|
||||
'Afbeelding kon niet worden gereconstrueerd';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Beeld ontvangen — beeldontcipheringsapparatuur is uitgeschakeld';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Afbeelding niet langer opgeslagen';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Probeer het opnieuw';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Decode opnieuw';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Inricht';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Tik om te verwerken';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'pakketten',
|
||||
one: 'pakket',
|
||||
);
|
||||
return '$bytes bytes · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds seconden';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes m $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,6 +371,47 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Ustawienia radia zostały zaktualizowane';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regiony';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Zarządzanie zapisanymi regionami';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Zarządzanie Regionami';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Wprowadź nazwę regionu';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Dodaj region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Pobierz regiony z repeaterów';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Nie znaleziono żadnych regionów';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Ten region został już dodany';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Nazwa Regionu';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Region usunięto';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Usuń Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Czy usunąć \"$region\" z listy regionów?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokalizacja';
|
||||
|
||||
@@ -469,6 +510,14 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Uwzględnij lokalizację w ogłoszeniu';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatyczne ogłoszenie zero-hop przy aktualizacji GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
|
||||
|
||||
@@ -579,6 +628,12 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Liczba kanałów';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Sprzęt';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Oprogramowanie wbudowane';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Presety';
|
||||
|
||||
@@ -695,6 +750,18 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukraińska';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 bajt';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bity';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bajty';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bajty';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
|
||||
|
||||
@@ -842,6 +909,9 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'Bateria LiPo HV (3,0-4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Wyświetlanie mapy';
|
||||
|
||||
@@ -898,6 +968,28 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Ostatni tydzień';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Źródło kafelków rastrowych';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Punkt końcowy Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Klucz API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Wymagane do korzystania ze Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Skonfigurowano: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Pamięć podręczna map offline';
|
||||
|
||||
@@ -1338,6 +1430,20 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'np. #zespół';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: brak';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Wybierz region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Przejrzyste obszar';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Brak jeszcze wiadomości';
|
||||
|
||||
@@ -1394,6 +1500,15 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Wyślij GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Wyślij obraz';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Nie mogłem otworzyć tego obrazu';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Otrzymano GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpowiedz';
|
||||
|
||||
@@ -2051,6 +2166,42 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
return 'Nieudane pobrania: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2595,7 +2746,43 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych – wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.';
|
||||
'Bajty używane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Zmiana kluczy tożsamości';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Zmień parę kluczy publiczną/prywatną';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Klucz prywatny';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Nowy klucz prywatny dla wzmacniacza, 128-znakowa string hexadecyimalny.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Wygeneruj losowy klucz parowy';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Przerwij wyszukiwanie kluczowego paru';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Klucz publiczny';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'To jest klucz publiczny, który odpowiada wygenerowanemu kluczowi prywatnemu. Nie można go ustawić bezpośrednio.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Pożądane przedrostek';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Znajdź klucz publiczny, który zaczyna się od tych cyfr hex. Oczekiwana liczba prób: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Opóźnienie w Flood, TX';
|
||||
@@ -4364,6 +4551,44 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Tłumaczenie';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Włącz wiadomości ze zdjęciami';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Przesyłaj zdjęcia przez siatkę. Wymaga jednorazowego pobrania modelu obrazu.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Model obrazowy';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Pobierz';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Anuluj';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Usuń model';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Gotowy';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Nie opublikowane — ten build nie może tego pobrać.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Model obrazowy nie mógł zostać pobrany.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Obrazki są przetwarzane automatycznie.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Odbuduj każde zdjęcie, jak tylko przyjdzie. Zużywa około 2 GB pamięci za każdą drugą sekundę; wyłącz odbudowywanie jednym dotknięciem.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Włącz tłumaczenie';
|
||||
|
||||
@@ -4652,4 +4877,201 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Przestań śledzić pakiet';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Wyślij obraz';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Zmniejszone do 512 × 512 · proporcje nie zostały zachowane';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Oryginalny';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'W eterze';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Jakość';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Wysoko';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pakiet';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Czas w eterze';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Payload';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pakietów',
|
||||
one: 'pakiet',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Ustawienia radia nieznane';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Połącz urządzenie, aby można było obliczyć czas trwania transmisji.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Długa transmisja';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Ten kanał będzie dostępny przez około $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Przepływ pakietów: każdy retransmitujący w zasięgu przekazuje każdy pakiet, przez co kanał pozostaje zajęty dłużej niż ten czas.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Pakiet odzysku';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Jedna dodatkowa paczka. Wiadomości grupowe nie są potwierdzane, dzięki czemu odbiorca może odbudować obraz, jeśli zostanie utracona pojedyncza paczka.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Wyślij';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Anuluj';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Ten obraz nie mógł zostać zakodowany.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Model obrazowy jest nadal pobierany.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Wysyłanie obrazu nie jest dostępne na tym urządzeniu.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Wiadomości ze zdjęciami są wyłączone w ustawieniach.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Ten radiodysk nie może wysyłać pakietów obrazu. Podłącz urządzenie z oprogramowaniem firmware 13 lub nowszym.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Obrazy są przesyłane jako dane grupowe, więc można je wysłać tylko do kanału — nie w wiadomości prywatnej.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Obraz zakodowany do więcej pakietów niż pozwala format siatki.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pakietów',
|
||||
one: 'pakiet',
|
||||
);
|
||||
return 'Obraz wysłany jako $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Obraz nie mogł zostać wysłany: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Wysyłanie obrazu — pakiet $sent z $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Węzeł $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'Otrzymano: $received z $total pakietów';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Oczekiwanie na rozszyfrowanie';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Kliknij, aby odszyfrować';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Odtwarzanie… około 1 sekundy';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Obraz niepełny — $received z $total pakietów dotarło';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Obraz nie może zostać zrekonstruowany';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Otrzymano obraz — dekodowanie obrazu jest nieprawidłowe.';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Obraz nie jest już przechowywany';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Spróbuj ponownie';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Zdekoduj ponownie';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Ustaw';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Kliknij, aby przetworzyć';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'pakiety',
|
||||
one: 'pakiet',
|
||||
);
|
||||
return '$bytes bajtów · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds sekund';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes minut $seconds sekundy';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,6 +369,46 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Configurações de rádio atualizadas';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regiões';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Gerenciar regiões armazenadas';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Gestão de Região';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Nome da região';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Adicionar região';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions =>
|
||||
'Regiões de obtenção a partir de repetidores';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Nenhuma região foi encontrada';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Esta região já foi adicionada';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Nome da Região';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Região excluída';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Excluir Região';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Remover \"$region\" da lista de regiões?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Localização';
|
||||
|
||||
@@ -466,6 +506,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Incluir localização no anúncio';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Anúncio zero-hop automático na atualização do GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Multi-ACKs';
|
||||
|
||||
@@ -577,6 +625,12 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Número do Canal';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Configurações pré-definidas';
|
||||
|
||||
@@ -692,6 +746,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ucraniano';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bytes';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bytes';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Ativar rastreamento de mensagens';
|
||||
@@ -839,6 +905,9 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Exibição do Mapa';
|
||||
|
||||
@@ -895,6 +964,28 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Da última semana';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Fonte de blocos raster';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Endpoint da Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Chave da API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obrigatório para usar o Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Configurado: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
|
||||
|
||||
@@ -1326,6 +1417,20 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'ex. #equipe';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Região: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Região: nenhuma';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Escolha uma região';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Região desimpedida';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Ainda não existem mensagens.';
|
||||
|
||||
@@ -1381,6 +1486,15 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Enviar GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Enviar imagem';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Não consegui abrir essa imagem';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Recebi um GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Responder';
|
||||
|
||||
@@ -2034,6 +2148,42 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
return 'Downloads falhas: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2580,7 +2730,43 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).';
|
||||
'Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Alterar Chaves de Identidade';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Altere a chave pública/chave privada';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Chave privada';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Uma nova chave privada para o repetidor, uma string hexadecimal de 128 caracteres.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Gere um par de chaves aleatório';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Interrompa a busca por par de chaves';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Chave pública';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Esta é a chave pública que acompanha a chave privada gerada. Você não pode definir isso diretamente.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Prefixo desejado';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Encontre uma chave pública que comece com esses dígitos hexadecimais. Número esperado de tentativas: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Atraso na entrega em Flood, TX';
|
||||
@@ -4342,6 +4528,44 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Tradução';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Mensagens de imagem';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Envie as imagens pela malha. Requer o download de um modelo de imagem único.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Modelo de imagem';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Download';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Cancelar';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Remover modelo';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Pronto';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Ainda não publicado — esta versão não pode baixá-lo.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'O modelo de imagem não pôde ser baixado.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Processar imagens automaticamente';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Reconstrua cada imagem assim que ela chegar. Utiliza cerca de 2 GB de memória por segundo; dispense a reconstrução com um toque.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Ativar a tradução';
|
||||
|
||||
@@ -4624,4 +4848,202 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Liberar vista do pacote';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Enviar imagem';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Redimensionado para 512 × 512 · a proporção de aspecto não foi preservada';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Texto Original';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'Ao ar';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Qualidade';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Padrão';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Alto';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pacotes';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Tempo em transmissão';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Carga útil';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pacotes',
|
||||
one: 'pacote',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle =>
|
||||
'Configurações de rádio desconhecidas';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Conecte-se a um dispositivo para que o horário de transmissão possa ser calculado.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Transmissão longa';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Isso manterá o canal por cerca de $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Roteamento de inundação: cada repetidor na faixa retransmite cada pacote, fazendo com que o canal permaneça ocupado por mais tempo do que isso.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Pacote de recuperação';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Um pacote extra. As mensagens em grupo não são reconhecidas, assim permitindo que o destinatário reconstrua a imagem caso um pacote seja perdido.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Cancelar';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Esta imagem não pode ser codificada.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'O modelo de imagem ainda está baixando.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'O envio de imagem não está disponível neste dispositivo.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'As mensagens de imagem estão desativadas nas configurações.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Este rádio não pode enviar pacotes de imagem. Conecte um dispositivo com firmware companheiro versão 13 ou superior.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'As imagens viajam como dados em grupo, portanto, só podem ser enviadas para um canal — não em mensagem direta.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Essa imagem foi codificada em mais pacotes do que o formato de malha permite.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'pacotes',
|
||||
one: 'pacote',
|
||||
);
|
||||
return 'Imagem enviada como $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'A imagem não pôde ser enviada: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Envio da imagem — pacote $sent de $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Nó $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received de $total pacotes';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Aguardando para decodificar';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Toque para decodificar';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Reconstruindo… cerca de 1 segundo';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Imagem incompleta — $received de $total pacotes chegaram';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'A imagem não pôde ser reconstruída';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Imagem recebida — decodificação de imagem incorreta';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Imagem não armazenada';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Tente novamente';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Decodifique novamente';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Configurar';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Toque para processar';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'pacotes',
|
||||
one: 'pacote',
|
||||
);
|
||||
return '$bytes bytes · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds segundos';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +368,48 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Настройки радио обновлены';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Регионы';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle =>
|
||||
'Управление сохранёнными регионами';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Управление регионами';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Введите название региона';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Добавить регион';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions =>
|
||||
'Извлекать регионы с ретрансляторов';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail =>
|
||||
'Не были найдены никакие регионы';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Этот регион уже был добавлен';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Название региона';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Регион удалён';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Удалить регион';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Удалить \"$region\" из списка регионов?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Позиция';
|
||||
|
||||
@@ -466,6 +508,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включить местоположение в объявление';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Авто-объявление без хопов при обновлении GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Несколько подтверждений';
|
||||
|
||||
@@ -576,6 +626,13 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Количество каналов';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Оборудование';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware =>
|
||||
'Производственное программное обеспечение';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Пресеты';
|
||||
|
||||
@@ -692,6 +749,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 байт';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 байта';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 байта';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 байта';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Включить трассировку сообщений';
|
||||
@@ -841,6 +910,9 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3.0–4.2 В)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0–4,35 В)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Отображение карты';
|
||||
|
||||
@@ -898,6 +970,28 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Последнюю неделю';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Источник растровых тайлов';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Конечная точка Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ключ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Требуется для использования Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Настроено: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
|
||||
|
||||
@@ -1327,6 +1421,20 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'например, #команда';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'РЕГИОН: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Регион: отсутствует';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Выберите регион';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Чистый регион';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Сообщений пока нет';
|
||||
|
||||
@@ -1382,6 +1490,15 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Отправить GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Отправьте изображение';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Не удалось открыть это изображение';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Получен GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Ответить';
|
||||
|
||||
@@ -2038,6 +2155,42 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
return 'Неудачных загрузок: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2585,7 +2738,42 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.';
|
||||
'Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Изменить ключи идентичности';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Измените пару публичных/частных ключей';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Личный ключ';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Для повторителя генерируется новый приватный ключ — шестнадцатеричная строка из 128 символов.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Сгенерируйте случайный ключ-пару';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Прервать поиск ключной пары';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Публичный ключ';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Это публичный ключ, который соответствует сгенерированному приватному ключу. Вы не можете установить его напрямую.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Желаемый префикс';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Найдите публичный ключ, начинающийся этими шестнадцатеричными цифрами. Ожидаемое количество попыток: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Задержка в работе системы Flood TX';
|
||||
@@ -4358,6 +4546,44 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Перевод';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Включить изображения в сообщения';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Отправьте изображения через сеть. Требуется однократная загрузка шаблона изображения.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Модель изображения';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Скачать';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Отмена';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Удалить модель';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Готов';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Пока не опубликовано — эта сборка не может скачать это.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Модель изображения не удалось скачать.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Автоматически обрабатывать изображения';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Примерно каждое изображение реконструируйте сразу после его поступления. Для этого на каждый раз используется около 2 ГБ памяти; отключите реконструкцию касанием.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Включить перевод';
|
||||
|
||||
@@ -4644,4 +4870,204 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Не следить за пакетом';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Отправьте изображение';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Меняется размер на 512 × 512 · соотношение сторон не сохраняется';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Оригинал';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'В эфире';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Качество';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Стандарт';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Высота';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Пакеты';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Время на эфире';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Пакет';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакета',
|
||||
many: 'пакетов',
|
||||
few: 'пакета',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Настройки радио неизвестны';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Подключитесь к устройству, чтобы можно было рассчитать время эфирного времени.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Долгая передача';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Это будет поддерживать канал примерно $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Руководство по маршрутизации потоков: каждый ретрансатор в зоне перераспределяет каждый пакет, поэтому канал остаётся занятным дольше, чем это.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Пакет восстановления';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Один дополнительный пакет. Групповые сообщения не подтверждаются, поэтому это позволяет получателю восстановить изображение, если один пакет потерян.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Отправить';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Отмена';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed =>
|
||||
'Это изображение не может быть закодировано.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Модель изображения всё ещё загружается.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Отправка изображения на этом устройстве недоступна.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'В настройках отключены сообщения с изображениями.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Это радио не может отправлять пакеты изображений. Подключите устройство, работающее на прошивке-компаньоне версии 13 или выше.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Изображения передаются как групповые данные, поэтому их можно отправлять только в канал, а не в личное сообщение.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Изображение было закодировано в больше пакетов, чем позволяет формат сетки.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакетов',
|
||||
one: 'пакет',
|
||||
);
|
||||
return 'Изображение отправлено как $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Изображение не может быть отправлено: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Отправка изображения — пакет $sent из $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Узел $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '$received из $total пакетов';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Ожидание расшифровки';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Нажмите для декодирования';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Реконструкция… примерно за 1 секунду';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Отсутствует изображение — $received из $total пакетов пришло';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Изображение не может быть восстановлено';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Получен изображение — декодирование изображения неправильное';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Изображение больше не сохраняется';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Попробуйте снова';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Декодируйте снова';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Настройте';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Нажмите для обработки';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'пакетов',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$bytes байт · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds секунд';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes м $seconds с';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +367,45 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regiony';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Zohľať uložené regiony';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regiónne vedenie';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Vstuptejte název regionu';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Dodajte region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Odberanie regionov z repeaterov';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Nebylo našli žiadné regiony';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Tento oblast je už dodán';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Regionné název';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Regionu zníteno';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Vyniknúcia regionu';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Odstraniť $region z listy regionov?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokalita';
|
||||
|
||||
@@ -460,6 +499,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatický zero-hop inzerát pri aktualizácii GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Viaceré ACK';
|
||||
|
||||
@@ -571,6 +618,12 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Počet kanálov';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Firmware';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Prednastavenia';
|
||||
|
||||
@@ -686,6 +739,18 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinská';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byt';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bity';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bity';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bity';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
|
||||
|
||||
@@ -828,6 +893,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Zobrazenie mapy';
|
||||
|
||||
@@ -885,6 +953,28 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Minul týždeň';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Zdroj rastrových dlaždíc';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Koncový bod Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Kľúč API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Vyžaduje sa na používanie Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Nakonfigurované: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
|
||||
|
||||
@@ -1316,6 +1406,20 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'napr. #tím';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: žiadné';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Vyberte region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Jasný údajný oblasť';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Zatiaľ žiadne správy.';
|
||||
|
||||
@@ -1371,6 +1475,15 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Odoslať GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Vyslat obraz';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Nesmiela jsem otvoriť tento obrázok';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Odeslaný GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odpovedať';
|
||||
|
||||
@@ -2024,6 +2137,42 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
return 'Neúspešné stiahnutia: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2566,7 +2715,43 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.';
|
||||
'Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Zmena identititačných klíčov';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Zmena prehľadného/osobného klíčového zbraň';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Prívatná klíč';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Nová prívna klíčová číslo pre repeater, 128-znáčlový šestnáctorýč.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Vytvorenie príma kľúčového paru';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Prerušenie hľadania klíčového paru';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Táto je veľká klíč, ktorý sa vztahuje s generovaným manéčným klíčom. Nemožné je to ustanovit priateľne.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Požadovaný prefiks';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Zahľadnite sa veľkú publicenu klíču, ktorá začíná tými šestícih štiastkami. Očekivané potrebné pokusy: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Zpoždanie v Flood, TX';
|
||||
@@ -4323,6 +4508,43 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Preklad';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Zabezpečte zobrazi';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Odeslanie obrazov pre mšťanie. Potrebujete jednorazové stáhnutie modelu obrazov.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Model v kínajme';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Stáhnie';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Zastavenie';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Vyhniť model';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Čiačný';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Nesprávne vypisáné — tento prechovka nie môže jej stáhnuť.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Model s obrázkom nemôžno sťažovať.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Automaticky prepojiť slučaje.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Rekonstruujte každý obraz tak, ako pridá. Používajete okolo 2 GB pamäti pre drugú hodinu; odložte rekonstrukciu tým kliknutím.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivovať preklad';
|
||||
|
||||
@@ -4608,4 +4830,202 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Odomknúť pohľad od paketu';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Vyslat obraz';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Zmeneno na 512 × 512 · vzorýst nie byl zachovan';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Výborný text:\n\n\"Language is the road map of a culture. It tells us where to go as a society and helps us understand the world around us. Learning a new language opens up new perspectives and opportunities, allowing us to connect with people from different backgrounds and build bridges between cultures.\"\n\nSlovenská preklada:\n\n\"Jazyk je cesta pre zručnosti a odráža kultúru. Dá nám uvedomi, dokádať, kúdy sa pohladneme a pomáha našej společnosti poradiť sa v rôznych okolnosti a porozumieť o svetu kolem nás. Nauka nového jazyka otviera nové perspektívy a možnosti, čo pohybuje našu schopnosť sa spoluprávať s ľudmi z rôznych okolnosti a budovať mosty medzi kultúry.\"';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'V prúce';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Kvalita';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Stándard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Vysoká';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pakety';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Čas na anteni';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Základ';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Nastavenia radio nezná';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Zkonečte s určým prijemcem, aby sa mohlo výpočet času, ktorý je v prúbežu.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Dlouhá transmisia';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Tento dôvod udrží kanál priamo na $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Povodný routing: každý repeater v rozsahu zrňa každý paket, takže kanál zostá zajetší dlouше, než tu.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Poučný paket pre očudenie';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Jedný dodatný paket. Skúsené súhlasy nie sa uznávaú, takže to umožňuje prechovávaču opraviť obraz, pokiaľ sa jedný paket niebyl ztrácený.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Odeslať';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Zastavenie';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Tento obráz nie byla byla schována.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Model prehľadovania prebeža stále stále stále prechádza.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Zobrazenie nie je dostupný na tomto zariadení.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Zobrazy s prešteľkami sú v nastavení vypnené.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Tento radiok nie môže odeslať obrazové pakety. Podeďte správne určité zařízení s oprogramovaním spolupráce 13 alebo novším.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Obrazy sa prechádzajú jako skupinové údaje, preto sa ich môžu odeslať len na kanál — nie v príbeh sa.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Tento obraz byl skodyrovany do viac paketov, ako umožňuje si sítka format.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return 'Obrázok odoslaný ako $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Obraz nemôžlo odeslať: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Vysyťanie obrazu — pakét $sent z $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Vęzlo $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'Otrzymané $received z $total pakotí';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Čekanie na dekódovanie';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Kliknite, aby dekódovať';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Rekonstrukcia… o čoľve 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Obraz nieješťajný — $received z $total paciek príšlo';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Obraz nemôžu byť prepojený';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Otrzymaná slúba — dekódovanie slúby je vypadejce';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Obraz nemá uložený';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Zkôr to opítajte';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Zdeľte sa znovu';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Založte';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Ukliknite, aby sa príjomili';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$bytes bajtov · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -366,6 +366,46 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regioni';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Upravljanje opisanih regionov';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regijonsko upravljanje';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Naslov občine';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Dodaj region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions =>
|
||||
'Pobražujte izvajne področja iz ponavljalnikov';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Nižnih občin ni izkrali';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Taj oblasi je več pomanjkan.';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Naziv Regiona';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Oblikačena regija';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Prihoda Region';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Ostani $region iz listy regionov?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Lokacija';
|
||||
|
||||
@@ -461,6 +501,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Samodejni zero-hop oglas ob posodobitvi GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Več potrdil';
|
||||
|
||||
@@ -569,6 +617,12 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Število kanalov';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardver';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Pravilnik';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Prednastavitve';
|
||||
|
||||
@@ -684,6 +738,18 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrajinsko';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 bajt';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 bite';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 bite';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 bite';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
|
||||
|
||||
@@ -830,6 +896,9 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0–4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Prikaz zemljevida';
|
||||
|
||||
@@ -886,6 +955,28 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Prejšnji teden';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Vir rastrskih ploščic';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Končna točka Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ključ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Obvezno za uporabo Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Nastavljeno: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
|
||||
|
||||
@@ -1314,6 +1405,20 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'npr. #ekipa';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Osebno: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: brak';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Vablajte region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Jasna regija';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Še ni sporočil.';
|
||||
|
||||
@@ -1370,6 +1475,15 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Pošlji GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Sločaji slik';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Nispeval sem tega ta slik';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Otrvljen GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Odgovori';
|
||||
|
||||
@@ -2021,6 +2135,42 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
return 'Poslovniški izniki: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2562,7 +2712,43 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.';
|
||||
'Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Zmeničke ključe identitete';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Zamenjajte publican ali privatni ključni par';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Privarna ključa';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Nova privatna ključa za prekaznik, 128- znakovni šestnastik.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Sporočna ključna para';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey =>
|
||||
'Prekajanje odkrivanja ključne paro';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Public key (publična ključa)';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Taj je publicna ključ, ki pomeni pripovedno privatno ključ. Ne možeš to vrediti direktno.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Poželana prefiks';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Najdi publicen ključ, ki začne s tih hexnih čitov. Očekivani pokusi: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu';
|
||||
@@ -4322,6 +4508,43 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Prevod';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Pozivni izkričile slike';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Prešli vzorce pre mrežo. Potrebno je jednokratno stworiti model vzorca.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Obrazni model';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Storba';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Odločite';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Ubrani model';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Gotov';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Jo še niso izdana — ta izdelka te ne može prebaviti.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Obrazni model ne može biti preuzet.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Avtomatizirane slike';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Katero je oblikovanje svega oblikov, ko se pošteva. Za drugo okolo časa se koristi ok. 2 GB pamične prostore; odločite se za oblikovanje s tukom.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Omogočite prevod';
|
||||
|
||||
@@ -4609,4 +4832,202 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Odkleni pogled od paketa';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Sločaji slik';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Zadovoljno izmenjeno na 512 × 512 · vzorovni odnos ne ohranjen';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Original text:\n\nThe early bird catches the worm.\n\nTranslation:\n\nPojdi prvi, zlati v kroku.';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'Na vodi';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Kakovost';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Vysoko';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Pakete';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Čas v eteru';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Napravo';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Naslušniki radio neznane';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Poučajte se na stroj, da se lahko izračunuje čas v prikazu.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Dluga preposlana';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Ovo bo ohranilo kanal prihodno za obliko $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Povlatni potok: sve repeaterje v območju pretrasirajo svak paket, pa kanál ostane zanudnejši od tega.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Pobavni paket zdržbe';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Jedna dodatna paczka. Grupni izjasi ne izvajajo potvarnosti, pa ta izdelka omogoča preuzimanju ponovno izgleda, če je zgubjen pojedni paket.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Posyłaj';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Odločite';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Ovo slikovo ne more biti zakodirano.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Obrazni model še pospeva preborjanje.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Poslanje slike nema na tem zelo naročnem naročnem.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled => 'Obrazi s pomanjkanjem so v nastavnih.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Taj radio ne može posredovati slike pakuete. Prepoštijo izbirni napravnik s kompanijskimi programi na čevem 13 ali novšejšim.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Obrazy se pojavijo kot skupnih podatkov, pa lahko ih poslujejo samo na kanal — ne v prilogih.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Taj slik je zakodiran v viših pakotnih od športne formate, ki omogoča mrežni format.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return 'Slika poslana kot $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Obraz ne može biti poslany: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Prekazivanje izkaza — paket $sent od $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Vrtitev $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'Otrteno od $received na $total poslovanih potokov';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Čekanje, da se dekodira';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Kliknite, da razkrivete';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding =>
|
||||
'Ostvarjanje… o prihodnosti za okolo 1 s';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Obraz nije kompletan — $received od $total poslaničev je prishodil';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Obraz ne more lahko rekonstruirati';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Otrgena slika — dekodirana slika je izključena';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Obraz ni več niso zakajiren';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Pokušajte znova';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Dekodiraj ponovno';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Nastavite';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Kliknij za obravnavanje';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'paketov',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$bytes bajtov · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds s';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes m $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -365,6 +365,45 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get settings_radioSettingsUpdated =>
|
||||
'Radioinställningarna har uppdaterats';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Regioner';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Hämtade regioner';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Regionförvaltning';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Enter regionnamn';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Lag till region';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Hämtar regioner från repeater';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Ingen region hittades';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Denna region har redan lagt till';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Regionnavn';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Regionens kallt offentlig';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Förhålla regio';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Fjärna \"$region\" ur regionlistan?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Plats';
|
||||
|
||||
@@ -458,6 +497,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Automatisk zero-hop-annons vid GPS-uppdatering';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Flera bekräftelser';
|
||||
|
||||
@@ -566,6 +613,12 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Kanalantal';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Hardware';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Föredata';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Fördefinierade inställningar';
|
||||
|
||||
@@ -681,6 +734,18 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Ukrainska';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 byte';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 byte';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
|
||||
|
||||
@@ -823,6 +888,9 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Kartvisning';
|
||||
|
||||
@@ -879,6 +947,28 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Förra veckan';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Källa för rasterplattor';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia-slutpunkt';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API-nyckel';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Krävs för att använda Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Konfigurerad: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Offline Kartcache';
|
||||
|
||||
@@ -1306,6 +1396,20 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 't.ex. #team';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Region: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Region: ingen';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Välj en region';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'klar region';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Inga meddelanden ännu';
|
||||
|
||||
@@ -1363,6 +1467,15 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Skicka GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => 'Send vagnbild';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Kunde inte öppna det bildet';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Mottagit en GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Svara';
|
||||
|
||||
@@ -2009,6 +2122,42 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
return 'Misslyckade nedladdningar: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2550,7 +2699,42 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).';
|
||||
'Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Ändra identitetsnyckel';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Vänd nya offentliga/privata nyckelpar';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Privat nyckel';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'En ny privat nyckel för repeatern, en hexsträng med 128 tecken.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Generera en kära nyckelpaar';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Avbröka sökande efter nyckelpa';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Öffentlig nyckel';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Detta är den offentliga nyckeln som tillhör genererad privaten nyckel. Du kan inte setta den direkt.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Önskad prefix';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Hitta en offentlig nyckel som börjar med dessa hexadecimala siffror. Förväntad antal försök: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Försening i Flood TX';
|
||||
@@ -4295,6 +4479,44 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Översättning';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Bildermeddelanden';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Send bilder över mönstret. Det kräver en enkelt bildmodellhämtning.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Bildmodell';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Download';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Avbud';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Förhåll dig från modellen';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Färdig';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Ingen publikerad än — denna byggnads kan inte laddas ner av det.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Bildmodellen kunde inte laddas ner.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle =>
|
||||
'Automatiska bilder automatiserar';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Rekonstruera varje bild så snart den ankommer. Använder cirka 2 GB minne varje gång; låt det omför att rekonstruera genom ett tap.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Aktivera översättning';
|
||||
|
||||
@@ -4579,4 +4801,202 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Lås upp vy från paket';
|
||||
|
||||
@override
|
||||
String get imageSend_title => 'Send vagnbild';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Reserverat till 512 × 512 · förhållandet mellan storlek inte bevarat';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize =>
|
||||
'Ovanstående text (engelska)\n\nTranslation (svenskt):';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'Underför hjärnan';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Kvalitet';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Standard';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Hög';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Paket';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Tid i övrighet';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Ladning';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paket',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Radioindikatorer inte kända';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Förkoppna till ett enhet så kan tiden på air beräknas.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Långförsending';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Detta kommer att bevara kanalen roughly $duration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Flodledning: varje repeater i ramen retransmitterar varje paket, så kan kanalen vara på arbete långare än den här tiden.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Omedlningspaket';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'En ytterligare paket. Gruppmeddelanden inte akceras, så den här tar hänsyn till att mottar kan återuppbygga bilden om ett enkelt paket förlorats.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Sänd';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Avbud';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => 'Denna bild kan inte kodas.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Bildmodellen downloads fortfarande.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Bildsendning är inte tillgänglig på detta enhet.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Bildmeddelanden är offna i inställningarna.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Denna radi kan inte senda bildpaket. Använd ett äventyrsgerät med companionsystemfirmware 13 eller nyare.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Bilder senders som gruppod, så de kan endast skickas till ett kanal – inte i en direkt meddelning.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Bilden kodades till mer paket än meshformaten tillåter.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'paket',
|
||||
one: 'paket',
|
||||
);
|
||||
return 'Bilden skickades som $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Bilden kunde inte skickas: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Bild sendning — paket $sent av $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Nod $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'VANTA $received av $total paket';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Att väta att decodera';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Tap för att decodera';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Rekonstruktion… om 1 sekund';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Bilden är okomplett — $received av $total paket har ankomit';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Bilden kunde inte rekonstrueras';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Bild mottagen — bilddekoderingen är avbrott';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Bild inte lagrad längre';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Omförs igen';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Kodera igen';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Skapa en uppställning';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Tap för att behandla';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'paket',
|
||||
one: 'paket',
|
||||
);
|
||||
return '$bytes byte · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds sekunder';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes min $seconds s';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,6 +367,45 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => 'Ріси';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => 'Управляти збережених регіонів';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => 'Рégіональний управління';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => 'Назва регіону';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => 'Добачить регіон';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => 'Збираємо регіони з репітерів';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => 'Ні відкриті регіони';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists =>
|
||||
'Цей регіон вже було доданий';
|
||||
|
||||
@override
|
||||
String get settings_regionName => 'Ріонна назва';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => 'Ріон витертений';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => 'Видалення регіону';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return 'Вирішіть \"$region\" з списку регіонів?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => 'Геопозиція';
|
||||
|
||||
@@ -462,6 +501,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get settings_advertLocationSubtitle =>
|
||||
'Включити геопозицію в оголошення';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate =>
|
||||
'Автооголошення без хопів при оновленні GPS';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => 'Багато підтверджень';
|
||||
|
||||
@@ -574,6 +621,12 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => 'Кількість каналів';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => 'Оборудовання';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => 'Програмне вміст (фirmware)';
|
||||
|
||||
@override
|
||||
String get settings_presets => 'Попередні налаштування';
|
||||
|
||||
@@ -689,6 +742,18 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => 'Українська';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 байт';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 байти';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 байти';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 байта';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing =>
|
||||
'Увімкнути відстеження повідомлень';
|
||||
@@ -835,6 +900,9 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => 'ЛиPo HV (3,0-4,35 В)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => 'Відображення карти';
|
||||
|
||||
@@ -892,6 +960,28 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => 'Минулий тиждень';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => 'Джерело растрових тайлів';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Кінцева точка Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Ключ API Stadia';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired =>
|
||||
'Потрібно для використання Stadia Maps';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return 'Налаштовано: $maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
|
||||
|
||||
@@ -1320,6 +1410,20 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => 'напр. #команда';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return 'Ріон: $region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => 'Ріон: нема';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => 'Вybірте регіон';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => 'Чистя зона';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => 'Поки немає повідомлень.';
|
||||
|
||||
@@ -1376,6 +1480,16 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => 'Надіслати GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage =>
|
||||
'Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => 'Неможливо відкрити це изображення';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => 'Підймовано GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => 'Відповісти';
|
||||
|
||||
@@ -2031,6 +2145,42 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
return 'Невдалі завантаження: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2582,7 +2732,42 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.';
|
||||
'Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => 'Зміна ключів ідентифікації';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle =>
|
||||
'Замініть публічний/приватний ключ';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => 'Приватний ключ';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper =>
|
||||
'Нова приватна ключ для репітера, 128-символова штучна штука в шестнадцатеричному форматі.';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => 'Синхронізujте ключовий пару';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => 'Прервити шук ключового пару';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => 'Публічна ключ';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper =>
|
||||
'Це публична ключ, який пов\'язано з створеною приватною ключом. Ви не можете встановлювати його напрямую.';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => 'Високорейтингове прикінання';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return 'Найдіть відповідний открытий ключ, який починається ціми шестнадцатыми цифрами. Ожидається кількість потрібних попыток: $tries.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => 'Затримка у Flood, штат Техас';
|
||||
@@ -4355,6 +4540,43 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => 'Переклад';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => 'Підтримка зображень для посилання';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle =>
|
||||
'Пошукуєте изображення через сеть. Вимagaєте однократне скачання моделю изображення.';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => 'Модель для зображень';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => 'Скачання';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => 'Завершити';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => 'Видалим модель';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => 'Готово';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished =>
|
||||
'Щоб не було опубліковано — це версія не дозволяє зробити скачання.';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed =>
|
||||
'Модель для зображень не можна сказати.';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => 'Автоматично оброблювати obraзи';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'Кожна картинка повинні бути відтворена на мій момент прибуття. Використовує приблизно 2 ГБ пам\'яті на кожен раз; відключіть від відтворення за допомогою кlikу.';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => 'Увімкнути переклад';
|
||||
|
||||
@@ -4644,4 +4866,205 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Відв\'язати вигляд від пакету';
|
||||
|
||||
@override
|
||||
String get imageSend_title =>
|
||||
'Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote =>
|
||||
'Манітовано до 512 × 512 · соотношення варість не зберігається';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => 'Оригінал';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => 'На ері';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => 'Качество';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => 'Стандарт';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => 'Висок';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => 'Пакети';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => 'Час на ері';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => 'Підлог';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакета',
|
||||
many: 'пакетів',
|
||||
few: 'пакети',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => 'Настройки радио неизвестні';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody =>
|
||||
'Залучіться до пристрою, щоб можна було розрахунювати час в етері.';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => 'Длуга передача';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return 'Це забезпечить канал на approximativno $duration хвилин.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote =>
|
||||
'Рухування потоків: кожен ретрансатор у діапазоні відправлює кожен пакет, тому канал залишається занятшим довше, ніж це.';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => 'Пакет відновлення';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'Один дополний пакет. Групові посилання не відповідають, тому це дозволяє відвідувачу відтворити зображення, якщо один пакет втрачений.';
|
||||
|
||||
@override
|
||||
String get imageSend_send => 'Пошутить';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => 'Завершити';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed =>
|
||||
'Це изображення не може бути кодировано.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading =>
|
||||
'Модель для відтворення изображень ще збирається.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable =>
|
||||
'Постій для відправлення изображень на цьому пристрої не доступний.';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled =>
|
||||
'Пictesovani posyly wyключені в настройках.';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'Цей радіопередач не може отправляти пакети зображень. Зконнете пристрої, що працює з обладнанням керемонітом компаньєр 13 або новшим.';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'Фотографії передаються як дані групою, тому їх можна відправляти лише на канал — не в прямому посилання.';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge =>
|
||||
'Це изображення кодує більше пакітів, ніж дозволяє формат сети.';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: 'пакетів',
|
||||
one: 'пакет',
|
||||
);
|
||||
return 'Зображення надіслано як $count $_temp0.';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return 'Изображення не можна відправити: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return 'Пошускаємо изображення — пакет $sent з $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return 'Нод $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return 'Підходяче: $received з $total пакітів';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => 'Очікування розслівання коду';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => 'Кliknite для декодування';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => 'Повноювання… за прибуття 1 с.';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return 'Образок не повний — $received з $total пакітів прибавлено';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => 'Obraz не можна відтворити';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing =>
|
||||
'Під час приємки — розшифровка зображення не вдалося.';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => 'Изображення не зберігається';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => 'Попробуй ще раз';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => 'Декодуй ще раз';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => 'Заведіть';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => 'Кликніть для обробки';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: 'пакетів',
|
||||
one: 'пакет',
|
||||
);
|
||||
return '$bytes байтів · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds секунди';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes хв $seconds с';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -350,6 +350,44 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_radioSettingsUpdated => '无线电设置已更新';
|
||||
|
||||
@override
|
||||
String get settings_regionSettings => '地区';
|
||||
|
||||
@override
|
||||
String get settings_regionSettingsSubtitle => '管理已存储的区域';
|
||||
|
||||
@override
|
||||
String get settings_regionManagement_screenTitle => '区域管理';
|
||||
|
||||
@override
|
||||
String get settings_regionNameHint => '区域名称';
|
||||
|
||||
@override
|
||||
String get settings_regionAddRegion => '增加地区';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegions => '从中继器获取区域';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsFail => '未发现任何地区';
|
||||
|
||||
@override
|
||||
String get settings_regionFetchRegionsAlreadyExists => '该地区已被添加';
|
||||
|
||||
@override
|
||||
String get settings_regionName => '地区名称';
|
||||
|
||||
@override
|
||||
String get settings_regionDeleted => '区域已删除';
|
||||
|
||||
@override
|
||||
String get settings_deleteRegion => '删除区域';
|
||||
|
||||
@override
|
||||
String settings_deleteRegionConfirm(String region) {
|
||||
return '从 $region 列表中删除?';
|
||||
}
|
||||
|
||||
@override
|
||||
String get settings_location => '位置';
|
||||
|
||||
@@ -437,6 +475,13 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_advertLocationSubtitle => '在广告中包含位置';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS 更新时自动发送零跳广告';
|
||||
|
||||
@override
|
||||
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
|
||||
'当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。';
|
||||
|
||||
@override
|
||||
String get settings_multiAck => '多重ACK';
|
||||
|
||||
@@ -541,6 +586,12 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get settings_infoChannelCount => '频道数量';
|
||||
|
||||
@override
|
||||
String get settings_infoHardware => '硬件';
|
||||
|
||||
@override
|
||||
String get settings_infoFirmware => '固件';
|
||||
|
||||
@override
|
||||
String get settings_presets => '预设';
|
||||
|
||||
@@ -655,6 +706,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_languageUk => '乌克兰语';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption0 => '0 - 1 字节';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption1 => '1 - 2 字节';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption2 => '2 - 3 字节';
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeOption3 => '3 - 4 字节';
|
||||
|
||||
@override
|
||||
String get appSettings_enableMessageTracing => '启用消息追踪';
|
||||
|
||||
@@ -779,6 +842,9 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)';
|
||||
|
||||
@override
|
||||
String get appSettings_batteryLipoHv => '锂多压电源(3.0-4.35伏)';
|
||||
|
||||
@override
|
||||
String get appSettings_mapDisplay => '地图显示';
|
||||
|
||||
@@ -832,6 +898,27 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get appSettings_lastWeek => '上周';
|
||||
|
||||
@override
|
||||
String get appSettings_rasterTileSource => '栅格瓦片源';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaEndpoint => 'Stadia 端点';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKey => 'Stadia API 密钥';
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyRequired => '使用 Stadia Maps 时必需';
|
||||
|
||||
@override
|
||||
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
|
||||
return '已配置:$maskedKey';
|
||||
}
|
||||
|
||||
@override
|
||||
String get appSettings_stadiaApiKeyDialogDescription =>
|
||||
'请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。';
|
||||
|
||||
@override
|
||||
String get appSettings_offlineMapCache => '离线地图缓存';
|
||||
|
||||
@@ -1245,6 +1332,20 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get channels_hashtagHint => '例如:#团队';
|
||||
|
||||
@override
|
||||
String channels_regionSetTo(String region) {
|
||||
return '地区:$region';
|
||||
}
|
||||
|
||||
@override
|
||||
String get channels_regionNotSet => '地区:无';
|
||||
|
||||
@override
|
||||
String get channels_regionSelect_Title => '选择一个地区';
|
||||
|
||||
@override
|
||||
String get channels_clearRegion => '无碍区域';
|
||||
|
||||
@override
|
||||
String get chat_noMessages => '暂无消息';
|
||||
|
||||
@@ -1300,6 +1401,15 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get chat_sendGif => '发送 GIF';
|
||||
|
||||
@override
|
||||
String get chat_sendImage => '发送图片';
|
||||
|
||||
@override
|
||||
String get chat_imagePickFailed => '无法打开该图片';
|
||||
|
||||
@override
|
||||
String get chat_receivedGif => '收到一个GIF';
|
||||
|
||||
@override
|
||||
String get chat_reply => '回复';
|
||||
|
||||
@@ -1925,6 +2035,42 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
return '下载失败:$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTilesLabel => 'Cached tiles';
|
||||
|
||||
@override
|
||||
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledForSource(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_bulkDownloadDisabledInConfig(String source) {
|
||||
return 'Offline bulk downloads are disabled for $source in this app configuration.';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summarySource(String source) {
|
||||
return 'Source: $source';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedTilesForSource(int count) {
|
||||
return 'Cached tiles for source: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryCachedInSelection(int count) {
|
||||
return 'Cached in selected area/zoom: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_summaryApproxCacheSize(String size) {
|
||||
return 'Approx cache size: $size';
|
||||
}
|
||||
|
||||
@override
|
||||
String mapCache_boundsLabel(
|
||||
String north,
|
||||
@@ -2439,7 +2585,39 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get repeater_pathHashModeHelper =>
|
||||
'用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。';
|
||||
'用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 跳),1=2 字节(65K 个 ID,最多 32 跳),2=3 字节(16M 个 ID,最多 21 跳)。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。';
|
||||
|
||||
@override
|
||||
String get repeater_keySettings => '更改身份密钥';
|
||||
|
||||
@override
|
||||
String get repeater_keySettingsSubtitle => '更换公钥/私钥对';
|
||||
|
||||
@override
|
||||
String get repeater_prvKey => '私钥';
|
||||
|
||||
@override
|
||||
String get repeater_prvKeyHelper => '中继器的新私钥,为一个128位的十六进制字符串。';
|
||||
|
||||
@override
|
||||
String get repeater_generatePrvKey => '生成一个随机的密钥对';
|
||||
|
||||
@override
|
||||
String get repeater_stopGeneratingPrvKey => '中断密钥对搜索';
|
||||
|
||||
@override
|
||||
String get repeater_pubKey => '公钥';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyHelper => '这是与生成的私钥配对的公钥。您不能直接设置此内容。';
|
||||
|
||||
@override
|
||||
String get repeater_pubKeyPrefix => '期望的前缀';
|
||||
|
||||
@override
|
||||
String repeater_pubKeyPrefixHelper(int tries) {
|
||||
return '找到一组以这些十六进制数字开头的公钥。预计尝试次数:$tries';
|
||||
}
|
||||
|
||||
@override
|
||||
String get repeater_txDelay => '洪水(德克萨斯州)延误';
|
||||
@@ -4014,6 +4192,40 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get translation_title => '翻译';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableTitle => '启用图片消息';
|
||||
|
||||
@override
|
||||
String get imageMessages_enableSubtitle => '通过网格发送图像。需要一次性下载图像模型。';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelSectionTitle => '图像模型';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadModel => '下载';
|
||||
|
||||
@override
|
||||
String get imageMessages_cancelDownload => '取消';
|
||||
|
||||
@override
|
||||
String get imageMessages_removeModel => '移除模型';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelReady => '准备就绪';
|
||||
|
||||
@override
|
||||
String get imageMessages_modelNotPublished => '尚未发布——此版本无法下载该内容。';
|
||||
|
||||
@override
|
||||
String get imageMessages_downloadFailed => '图像模型无法下载。';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessTitle => '自动处理图像';
|
||||
|
||||
@override
|
||||
String get imageMessages_autoProcessSubtitle =>
|
||||
'每次图像到达后立即重建,每次需要约2GB的内存;若想关闭重建功能,可通过点击取消开启。';
|
||||
|
||||
@override
|
||||
String get translation_enableTitle => '启用翻译功能';
|
||||
|
||||
@@ -4288,4 +4500,193 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => '解锁视图跟随';
|
||||
|
||||
@override
|
||||
String get imageSend_title => '发送图片';
|
||||
|
||||
@override
|
||||
String get imageSend_cropNote => '调整大小至512 × 512 · 宽高比未保留';
|
||||
|
||||
@override
|
||||
String get imageSend_originalSize => '原文';
|
||||
|
||||
@override
|
||||
String get imageSend_onAirSize => '直播中';
|
||||
|
||||
@override
|
||||
String get imageSend_quality => '质量';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityStandard => '标准';
|
||||
|
||||
@override
|
||||
String get imageSend_qualityHigh => '高';
|
||||
|
||||
@override
|
||||
String get imageSend_packetsLabel => '数据包';
|
||||
|
||||
@override
|
||||
String get imageSend_airtimeLabel => '播出时间';
|
||||
|
||||
@override
|
||||
String get imageSend_sizeLabel => '有效载荷';
|
||||
|
||||
@override
|
||||
String imageSend_packetsCount(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '个数据包',
|
||||
one: '个数据包',
|
||||
);
|
||||
return '$count $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_range(String min, String max) {
|
||||
return '$min–$max';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_unknownValue => '—';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownTitle => '无线电设置未知';
|
||||
|
||||
@override
|
||||
String get imageSend_radioUnknownBody => '连接设备以计算播出时间。';
|
||||
|
||||
@override
|
||||
String get imageSend_longSendTitle => '长距离传输';
|
||||
|
||||
@override
|
||||
String imageSend_longSendBody(String duration) {
|
||||
return '这将维持频道大约 $duration 时间。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get imageSend_floodNote => '洪水路由:每个中继站都会转发每个数据包,因此信道比这段时间更长地处于繁忙状态。';
|
||||
|
||||
@override
|
||||
String get imageSend_parityTitle => '恢复包';
|
||||
|
||||
@override
|
||||
String get imageSend_paritySubtitle =>
|
||||
'多一个数据包。群发消息不被确认,因此如果某个数据包丢失,接收方可以重新构建图像。';
|
||||
|
||||
@override
|
||||
String get imageSend_send => '发送';
|
||||
|
||||
@override
|
||||
String get imageSend_cancel => '取消';
|
||||
|
||||
@override
|
||||
String get imageSend_encodeFailed => '此图像无法编码。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDownloading => '图像模型仍在下载中。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecUnavailable => '此设备无法发送图片。';
|
||||
|
||||
@override
|
||||
String get imageSend_codecDisabled => '图片消息在设置中已关闭。';
|
||||
|
||||
@override
|
||||
String get imageSend_deviceUnsupported =>
|
||||
'此无线电无法发送图像数据包。请连接运行伴侣固件13或更高版本的设备。';
|
||||
|
||||
@override
|
||||
String get imageSend_directMessagesUnsupported =>
|
||||
'图片作为群组数据传输,因此只能发送到频道,而不能直接发送给个人。';
|
||||
|
||||
@override
|
||||
String get imageSend_tooLarge => '该图像编码的数据包数量超过了网格格式所允许的范围。';
|
||||
|
||||
@override
|
||||
String imageSend_sentConfirmation(int count) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '个数据包',
|
||||
one: '个数据包',
|
||||
);
|
||||
return '图像已作为 $count $_temp0 发送。';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendFailed(String error) {
|
||||
return '无法发送图片:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_sendingProgress(int sent, int total) {
|
||||
return '发送图像 — 已发送 $sent 包含 $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_senderPrefix(String prefix) {
|
||||
return '节点 $prefix';
|
||||
}
|
||||
|
||||
@override
|
||||
String receivedImage_incoming(int received, int total) {
|
||||
return '已收到的 $received 个包含在 $total 个包裹中';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_queued => '等待解码';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToDecode => '点击解锁';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoding => '重建……大约1秒';
|
||||
|
||||
@override
|
||||
String receivedImage_incomplete(int received, int total) {
|
||||
return '图片不完整 — $received 已收到 $total 个数据包';
|
||||
}
|
||||
|
||||
@override
|
||||
String get receivedImage_corrupt => '图像无法重建';
|
||||
|
||||
@override
|
||||
String get receivedImage_decoderMissing => '收到图片 — 图像解码失败';
|
||||
|
||||
@override
|
||||
String get receivedImage_evicted => '图片已不再存储';
|
||||
|
||||
@override
|
||||
String get receivedImage_retry => '再试一次';
|
||||
|
||||
@override
|
||||
String get receivedImage_decodeAgain => '重新解码';
|
||||
|
||||
@override
|
||||
String get receivedImage_openSettings => '建立';
|
||||
|
||||
@override
|
||||
String get receivedImage_tapToProcess => '点击处理';
|
||||
|
||||
@override
|
||||
String receivedImage_awaiting(int bytes, int packets) {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
packets,
|
||||
locale: localeName,
|
||||
other: '个数据包',
|
||||
one: '个数据包',
|
||||
);
|
||||
return '$bytes 字节 · $packets $_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_secondsValue(String seconds) {
|
||||
return '$seconds 秒';
|
||||
}
|
||||
|
||||
@override
|
||||
String imageSend_minutesSecondsValue(String minutes, String seconds) {
|
||||
return '$minutes 分 $seconds 秒';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
|
||||
"settings_latitude": "Breedtegraad",
|
||||
"settings_longitude": "Lengtegraad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische zero-hop-advertentie bij GPS-update",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wanneer de GPS-locatie verandert, een zero-hop-advertentie verzenden (vereist locatie in advertentie).",
|
||||
"settings_privacyMode": "Privacy-modus",
|
||||
"settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
|
||||
"settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Afgelopen 6 uur",
|
||||
"appSettings_last24Hours": "Afgelopen 24 uur",
|
||||
"appSettings_lastWeek": "Afgelopen week",
|
||||
"appSettings_rasterTileSource": "Rastertegelbron",
|
||||
"appSettings_stadiaEndpoint": "Stadia-eindpunt",
|
||||
"appSettings_stadiaApiKey": "Stadia API-sleutel",
|
||||
"appSettings_stadiaApiKeyRequired": "Vereist voor gebruik van Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Geconfigureerd: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Voer je Stadia Maps API-sleutel in. De app gebruikt die voor rastertegelverzoeken.",
|
||||
"appSettings_offlineMapCache": "Offline Kaartcache",
|
||||
"appSettings_noAreaSelected": "Geen gebied geselecteerd",
|
||||
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1107,7 +1172,7 @@
|
||||
"repeater_advancedSettings": "Geavanceerd",
|
||||
"repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers",
|
||||
"repeater_pathHashMode": "Hash-modus voor paden",
|
||||
"repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Versies 1.13 en ouder gebruiken multi-byte paden – alleen na het activeren van het netwerk.",
|
||||
"repeater_pathHashModeHelper": "Bytes die gebruikt worden om de ID van deze repeater te coderen in flood-pad/lusdetectietags. 0=1 byte (256 ID's, tot 64 hops), 1=2 bytes (65.000 ID's, tot 32 hops), 2=3 bytes (16 miljoen ID's, tot 21 hops). Firmware vóór v1.14 gebruikte altijd 1-byte paden; v1.14 en nieuwer kunnen worden ingesteld op 2- of 3-byte paden.",
|
||||
"repeater_txDelay": "Vertraging bij Flood TX",
|
||||
"repeater_txDelayHelper": "Herzendinterval voor verkeer tijdens overstromingen, als een veelvoud van de tijd die het pakket nodig heeft (0-2, standaard 0.5). Een hoger getal betekent minder botsingen, maar ook een langere leveringstijd.",
|
||||
"repeater_directTxDelay": "Directe vertraging",
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Geen locatie",
|
||||
"pathMap_followPacket": "Weergave vergrendelen op pakket",
|
||||
"pathMap_unfollowPacket": "Weergave ontgrendelen van pakket",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regio's",
|
||||
"settings_regionSettingsSubtitle": "Beheer van opgeslagen regio's",
|
||||
"settings_regionManagement_screenTitle": "Regionale Bestuur",
|
||||
"settings_regionNameHint": "Regio naam invoeren",
|
||||
"settings_regionAddRegion": "Regio toevoegen",
|
||||
"settings_regionFetchRegions": "Haal regio's uit herhalers",
|
||||
"settings_regionFetchRegionsFail": "Er zijn geen regio's gevonden",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Deze regio is al toegevoegd.",
|
||||
"settings_regionName": "Regio Naam",
|
||||
"settings_regionDeleted": "Regio verwijderd",
|
||||
"settings_deleteRegion": "Regio verwijden",
|
||||
"settings_deleteRegionConfirm": "Verwijder \"{region}\" uit de regio-lijst?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bytes",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
|
||||
"channels_regionSetTo": "Regio: {region}",
|
||||
"channels_regionNotSet": "Regio: geen",
|
||||
"channels_regionSelect_Title": "Kies een regio",
|
||||
"channels_clearRegion": "Duidelijke regio",
|
||||
"chat_sendImage": "Beeld verzenden",
|
||||
"chat_imagePickFailed": "Kunde die afbeelding niet openen",
|
||||
"chat_receivedGif": "Ontvangen van een GIF",
|
||||
"repeater_keySettings": "Verander de Identiteitskies",
|
||||
"repeater_keySettingsSubtitle": "Wijzig de openbare/private sleutelpaar",
|
||||
"repeater_prvKey": "Geheime sleutel",
|
||||
"repeater_prvKeyHelper": "Een nieuwe private sleutel voor de repeater, een 128-karakter lange hexadecimale string.",
|
||||
"repeater_generatePrvKey": "Stel een willekeurige sleutelpaar op",
|
||||
"repeater_stopGeneratingPrvKey": "Stop zoek naar sleutelpaar",
|
||||
"repeater_pubKey": "Openbaar sleutel",
|
||||
"repeater_pubKeyHelper": "Dit is de openbare sleutel die past bij de gegenereerde privésleutel. U kunt dit niet direct instellen.",
|
||||
"repeater_pubKeyPrefix": "Verlangde voorvoegsel",
|
||||
"repeater_pubKeyPrefixHelper": "Zoek een openbare sleutel die begint met deze hexadecimale cijfers. Verwacht aantal pogingen: {tries}.",
|
||||
"imageMessages_enableTitle": "Bijbehorende afbeeldingen activeren",
|
||||
"imageMessages_enableSubtitle": "Beeldjes verzenden via het netwerk. Vereist eenmalig afdaling van een beeldmodel.",
|
||||
"imageMessages_modelSectionTitle": "Beeldmodel",
|
||||
"imageMessages_downloadModel": "Download",
|
||||
"imageMessages_cancelDownload": "Annuleren",
|
||||
"imageMessages_removeModel": "Model verwijderen",
|
||||
"imageMessages_modelReady": "Klaar",
|
||||
"imageMessages_modelNotPublished": "Nog niet gepubliceerd — deze versie kan dit niet downloaden.",
|
||||
"imageMessages_downloadFailed": "Het beeldmodel kon niet worden gedownload.",
|
||||
"imageMessages_autoProcessTitle": "Beeldverwerking automatisch",
|
||||
"imageMessages_autoProcessSubtitle": "Reconstrueer elk beeld direct nadat het binnenkomt. Het gebruikt ongeveer 2 GB geheugen voor elke keer; schakel het reconstrueren uit met een tik als je het niet nodig hebt.",
|
||||
"imageSend_title": "Beeld verzenden",
|
||||
"imageSend_cropNote": "Aangepast aan 512 × 512 · het aspectverhouding is niet behouden",
|
||||
"imageSend_originalSize": "Oorspronkelijk",
|
||||
"imageSend_onAirSize": "Op de lucht",
|
||||
"imageSend_quality": "Kwaliteit",
|
||||
"imageSend_qualityStandard": "Standaard",
|
||||
"imageSend_qualityHigh": "Hoog",
|
||||
"imageSend_packetsLabel": "Pakketten",
|
||||
"imageSend_airtimeLabel": "Tijd op de lucht",
|
||||
"imageSend_sizeLabel": "Belasting",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{pakket} other{pakketten}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Radio-instellingen onbekend",
|
||||
"imageSend_radioUnknownBody": "Verbinding maken met een apparaat zodat de uitzendtijd kan worden berekend.",
|
||||
"imageSend_longSendTitle": "Lange transmissie",
|
||||
"imageSend_longSendBody": "Dit houdt de kanaal ongeveer voor {duration} stand.",
|
||||
"imageSend_floodNote": "Overstromingsrouting: elke repeater binnen bereik herzendsends elk pakket opnieuw, waardoor het kanaal langer bezig blijft dan dit.",
|
||||
"imageSend_parityTitle": "Herstelpakket",
|
||||
"imageSend_paritySubtitle": "Een extra pakje. Groepsberichten worden niet bevestigd, waardoor de ontvanger het beeld kan herstellen als een enkel pakket verloren gaat.",
|
||||
"imageSend_send": "Stuur",
|
||||
"imageSend_cancel": "Annuleren",
|
||||
"imageSend_encodeFailed": "Dit beeld kon niet worden gecodeerd.",
|
||||
"imageSend_codecDownloading": "Het beeldmodel wordt nog steeds gedownload.",
|
||||
"imageSend_codecUnavailable": "Het verzenden van een afbeelding is niet mogelijk op dit apparaat.",
|
||||
"imageSend_codecDisabled": "Boodschappen met afbeeldingen zijn uitgeschakeld in de instellingen.",
|
||||
"imageSend_deviceUnsupported": "Dit radio kan geen beeldpakketten verzenden. Verbind een apparaat met de bijbehorende firmware versie 13 of nieuwer.",
|
||||
"imageSend_directMessagesUnsupported": "Afbeeldingen worden verzonden als groepsgegevens, dus ze kunnen alleen naar een kanaal worden gestuurd — niet via een directe boodschap.",
|
||||
"imageSend_tooLarge": "Die afbeelding werd gecodeerd in meer pakketten dan het mesh-formaat toelaat.",
|
||||
"imageSend_sentConfirmation": "Afbeelding verzonden als {count} {count, plural, =1{pakket} other{pakketten}}.",
|
||||
"imageSend_sendFailed": "Het beeld kon niet worden verzonden: {error}",
|
||||
"imageSend_sendingProgress": "Beeld verzenden — pakket {sent} van {total}",
|
||||
"receivedImage_senderPrefix": "Knipper {prefix}",
|
||||
"receivedImage_incoming": "{received} van de {total} pakketten",
|
||||
"receivedImage_queued": "Wachten op het ontcijferen",
|
||||
"receivedImage_tapToDecode": "Tik om te decoderen",
|
||||
"receivedImage_decoding": "Herstellen… ongeveer 1 seconde",
|
||||
"receivedImage_incomplete": "Afbeelding onvolledig — {received} van {total} pakketten ontvangen",
|
||||
"receivedImage_corrupt": "Afbeelding kon niet worden gereconstrueerd",
|
||||
"receivedImage_decoderMissing": "Beeld ontvangen — beeldontcipheringsapparatuur is uitgeschakeld",
|
||||
"receivedImage_evicted": "Afbeelding niet langer opgeslagen",
|
||||
"receivedImage_retry": "Probeer het opnieuw",
|
||||
"receivedImage_decodeAgain": "Decode opnieuw",
|
||||
"receivedImage_openSettings": "Inricht",
|
||||
"receivedImage_tapToProcess": "Tik om te verwerken",
|
||||
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pakket} other{pakketten}}",
|
||||
"imageSend_secondsValue": "{seconds} seconden",
|
||||
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
|
||||
"settings_latitude": "Szerokość",
|
||||
"settings_longitude": "Długość",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatyczne ogłoszenie zero-hop przy aktualizacji GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Gdy lokalizacja GPS się zmieni, wyślij ogłoszenie zero-hop (wymaga lokalizacji w ogłoszeniu).",
|
||||
"settings_privacyMode": "Tryb prywatności",
|
||||
"settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
|
||||
"settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i lokalizację w rozgłoszeniach.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Ostatnie 6 godzin",
|
||||
"appSettings_last24Hours": "Ostatnie 24 godziny",
|
||||
"appSettings_lastWeek": "Ostatni tydzień",
|
||||
"appSettings_rasterTileSource": "Źródło kafelków rastrowych",
|
||||
"appSettings_stadiaEndpoint": "Punkt końcowy Stadia",
|
||||
"appSettings_stadiaApiKey": "Klucz API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Wymagane do korzystania ze Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Skonfigurowano: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Wprowadź swój klucz API Stadia Maps. Aplikacja używa go do żądań kafelków rastrowych.",
|
||||
"appSettings_offlineMapCache": "Pamięć podręczna map offline",
|
||||
"appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.",
|
||||
"appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})",
|
||||
@@ -779,6 +794,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1117,7 +1182,7 @@
|
||||
"repeater_advancedSettings": "Zaawansowany",
|
||||
"repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów",
|
||||
"repeater_pathHashMode": "Tryb haszujący ścieżkę",
|
||||
"repeater_pathHashModeHelper": "Bity wykorzystywane do kodowania identyfikatora tego urządzenia w tagach ścieżek/detekcji pętli. 0=1 bity (256 identyfikatorów, do 64 skoków), 1=2 bity (65 000 identyfikatorów, do 32 skoków), 2=3 bity (1 600 000 identyfikatorów, do 21 skoków). Wersje 1.13 i wcześniejsze nie obsługują ścieżek wielobitowych – wykrywają tylko jedną, gdy sieć jest w wersji 1.14 lub nowszej.",
|
||||
"repeater_pathHashModeHelper": "Bajty używane do kodowania identyfikatora tego repeatera w tagach ścieżki flood/wykrywania pętli. 0=1 bajt (256 identyfikatorów, do 64 skoków), 1=2 bajty (65 000 identyfikatorów, do 32 skoków), 2=3 bajty (16 milionów identyfikatorów, do 21 skoków). Firmware sprzed v1.14 zawsze używał ścieżek 1-bajtowych; v1.14 i nowsze można skonfigurować na ścieżki 2- lub 3-bajtowe.",
|
||||
"repeater_txDelay": "Opóźnienie w Flood, TX",
|
||||
"repeater_txDelayHelper": "Ustawienie odstępu dla ruchu związanego z powodzią, jako mnożnik czasu przesyłania pakietu (0-2, domyślnie 0,5). Wyższe wartości oznaczają mniejszą liczbę kolizji, ale wolniejszą prędkość przesyłania.",
|
||||
"repeater_directTxDelay": "Bezpośrednie opóźnienie sygnału TX",
|
||||
@@ -2578,5 +2643,227 @@
|
||||
"pathMap_noLocation": "Brak lokalizacji",
|
||||
"pathMap_followPacket": "Śledź pakiet",
|
||||
"pathMap_unfollowPacket": "Przestań śledzić pakiet",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regiony",
|
||||
"settings_regionSettingsSubtitle": "Zarządzanie zapisanymi regionami",
|
||||
"settings_regionManagement_screenTitle": "Zarządzanie Regionami",
|
||||
"settings_regionNameHint": "Wprowadź nazwę regionu",
|
||||
"settings_regionAddRegion": "Dodaj region",
|
||||
"settings_regionFetchRegions": "Pobierz regiony z repeaterów",
|
||||
"settings_regionFetchRegionsFail": "Nie znaleziono żadnych regionów",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Ten region został już dodany",
|
||||
"settings_regionName": "Nazwa Regionu",
|
||||
"settings_regionDeleted": "Region usunięto",
|
||||
"settings_deleteRegion": "Usuń Region",
|
||||
"settings_deleteRegionConfirm": "Czy usunąć \"{region}\" z listy regionów?",
|
||||
"settings_infoHardware": "Sprzęt",
|
||||
"settings_infoFirmware": "Oprogramowanie wbudowane",
|
||||
"repeater_pathHashModeOption0": "0 - 1 bajt",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bity",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bajty",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bajty",
|
||||
"appSettings_batteryLipoHv": "Bateria LiPo HV (3,0-4,35 V)",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"channels_regionNotSet": "Region: brak",
|
||||
"channels_regionSelect_Title": "Wybierz region",
|
||||
"channels_clearRegion": "Przejrzyste obszar",
|
||||
"chat_sendImage": "Wyślij obraz",
|
||||
"chat_imagePickFailed": "Nie mogłem otworzyć tego obrazu",
|
||||
"chat_receivedGif": "Otrzymano GIF",
|
||||
"repeater_keySettings": "Zmiana kluczy tożsamości",
|
||||
"repeater_keySettingsSubtitle": "Zmień parę kluczy publiczną/prywatną",
|
||||
"repeater_prvKey": "Klucz prywatny",
|
||||
"repeater_prvKeyHelper": "Nowy klucz prywatny dla wzmacniacza, 128-znakowa string hexadecyimalny.",
|
||||
"repeater_generatePrvKey": "Wygeneruj losowy klucz parowy",
|
||||
"repeater_stopGeneratingPrvKey": "Przerwij wyszukiwanie kluczowego paru",
|
||||
"repeater_pubKey": "Klucz publiczny",
|
||||
"repeater_pubKeyHelper": "To jest klucz publiczny, który odpowiada wygenerowanemu kluczowi prywatnemu. Nie można go ustawić bezpośrednio.",
|
||||
"repeater_pubKeyPrefix": "Pożądane przedrostek",
|
||||
"repeater_pubKeyPrefixHelper": "Znajdź klucz publiczny, który zaczyna się od tych cyfr hex. Oczekiwana liczba prób: {tries}.",
|
||||
"imageMessages_enableTitle": "Włącz wiadomości ze zdjęciami",
|
||||
"imageMessages_enableSubtitle": "Przesyłaj zdjęcia przez siatkę. Wymaga jednorazowego pobrania modelu obrazu.",
|
||||
"imageMessages_modelSectionTitle": "Model obrazowy",
|
||||
"imageMessages_downloadModel": "Pobierz",
|
||||
"imageMessages_cancelDownload": "Anuluj",
|
||||
"imageMessages_removeModel": "Usuń model",
|
||||
"imageMessages_modelReady": "Gotowy",
|
||||
"imageMessages_modelNotPublished": "Nie opublikowane — ten build nie może tego pobrać.",
|
||||
"imageMessages_downloadFailed": "Model obrazowy nie mógł zostać pobrany.",
|
||||
"imageMessages_autoProcessTitle": "Obrazki są przetwarzane automatycznie.",
|
||||
"imageMessages_autoProcessSubtitle": "Odbuduj każde zdjęcie, jak tylko przyjdzie. Zużywa około 2 GB pamięci za każdą drugą sekundę; wyłącz odbudowywanie jednym dotknięciem.",
|
||||
"imageSend_title": "Wyślij obraz",
|
||||
"imageSend_cropNote": "Zmniejszone do 512 × 512 · proporcje nie zostały zachowane",
|
||||
"imageSend_originalSize": "Oryginalny",
|
||||
"imageSend_onAirSize": "W eterze",
|
||||
"imageSend_quality": "Jakość",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Wysoko",
|
||||
"imageSend_packetsLabel": "Pakiet",
|
||||
"imageSend_airtimeLabel": "Czas w eterze",
|
||||
"imageSend_sizeLabel": "Payload",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{pakiet} other{pakietów}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Ustawienia radia nieznane",
|
||||
"imageSend_radioUnknownBody": "Połącz urządzenie, aby można było obliczyć czas trwania transmisji.",
|
||||
"imageSend_longSendTitle": "Długa transmisja",
|
||||
"imageSend_longSendBody": "Ten kanał będzie dostępny przez około {duration}.",
|
||||
"imageSend_floodNote": "Przepływ pakietów: każdy retransmitujący w zasięgu przekazuje każdy pakiet, przez co kanał pozostaje zajęty dłużej niż ten czas.",
|
||||
"imageSend_parityTitle": "Pakiet odzysku",
|
||||
"imageSend_paritySubtitle": "Jedna dodatkowa paczka. Wiadomości grupowe nie są potwierdzane, dzięki czemu odbiorca może odbudować obraz, jeśli zostanie utracona pojedyncza paczka.",
|
||||
"imageSend_send": "Wyślij",
|
||||
"imageSend_cancel": "Anuluj",
|
||||
"imageSend_encodeFailed": "Ten obraz nie mógł zostać zakodowany.",
|
||||
"imageSend_codecDownloading": "Model obrazowy jest nadal pobierany.",
|
||||
"imageSend_codecUnavailable": "Wysyłanie obrazu nie jest dostępne na tym urządzeniu.",
|
||||
"imageSend_codecDisabled": "Wiadomości ze zdjęciami są wyłączone w ustawieniach.",
|
||||
"imageSend_deviceUnsupported": "Ten radiodysk nie może wysyłać pakietów obrazu. Podłącz urządzenie z oprogramowaniem firmware 13 lub nowszym.",
|
||||
"imageSend_directMessagesUnsupported": "Obrazy są przesyłane jako dane grupowe, więc można je wysłać tylko do kanału — nie w wiadomości prywatnej.",
|
||||
"imageSend_tooLarge": "Obraz zakodowany do więcej pakietów niż pozwala format siatki.",
|
||||
"imageSend_sentConfirmation": "Obraz wysłany jako {count} {count, plural, =1{pakiet} other{pakietów}}.",
|
||||
"imageSend_sendFailed": "Obraz nie mogł zostać wysłany: {error}",
|
||||
"imageSend_sendingProgress": "Wysyłanie obrazu — pakiet {sent} z {total}",
|
||||
"receivedImage_senderPrefix": "Węzeł {prefix}",
|
||||
"receivedImage_incoming": "Otrzymano: {received} z {total} pakietów",
|
||||
"receivedImage_queued": "Oczekiwanie na rozszyfrowanie",
|
||||
"receivedImage_tapToDecode": "Kliknij, aby odszyfrować",
|
||||
"receivedImage_decoding": "Odtwarzanie… około 1 sekundy",
|
||||
"receivedImage_incomplete": "Obraz niepełny — {received} z {total} pakietów dotarło",
|
||||
"receivedImage_corrupt": "Obraz nie może zostać zrekonstruowany",
|
||||
"receivedImage_decoderMissing": "Otrzymano obraz — dekodowanie obrazu jest nieprawidłowe.",
|
||||
"receivedImage_evicted": "Obraz nie jest już przechowywany",
|
||||
"receivedImage_retry": "Spróbuj ponownie",
|
||||
"receivedImage_decodeAgain": "Zdekoduj ponownie",
|
||||
"receivedImage_openSettings": "Ustaw",
|
||||
"receivedImage_tapToProcess": "Kliknij, aby przetworzyć",
|
||||
"receivedImage_awaiting": "{bytes} bajtów · {packets} {packets, plural, =1{pakiet} other{pakiety}}",
|
||||
"imageSend_secondsValue": "{seconds} sekund",
|
||||
"imageSend_minutesSecondsValue": "{minutes} minut {seconds} sekundy"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Latitude ou longitude inválidos.",
|
||||
"settings_latitude": "Latitude",
|
||||
"settings_longitude": "Longitude",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Anúncio zero-hop automático na atualização do GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando a localização GPS mudar, enviar um anúncio zero-hop (requer localização no anúncio).",
|
||||
"settings_privacyMode": "Modo de Privacidade",
|
||||
"settings_privacyModeSubtitle": "Esconder nome/localização em anúncios",
|
||||
"settings_privacyModeToggle": "Ative o modo de privacidade para ocultar seu nome e localização em anúncios.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Últimos 6 horas",
|
||||
"appSettings_last24Hours": "Últimas 24 horas",
|
||||
"appSettings_lastWeek": "Da última semana",
|
||||
"appSettings_rasterTileSource": "Fonte de blocos raster",
|
||||
"appSettings_stadiaEndpoint": "Endpoint da Stadia",
|
||||
"appSettings_stadiaApiKey": "Chave da API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obrigatório para usar o Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Insira sua chave da API Stadia Maps. O aplicativo a usa para solicitações de blocos raster.",
|
||||
"appSettings_offlineMapCache": "Cache de Mapa Offline",
|
||||
"appSettings_noAreaSelected": "Nenhuma área selecionada",
|
||||
"appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1107,7 +1172,7 @@
|
||||
"repeater_advancedSettings": "Avançado",
|
||||
"repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes",
|
||||
"repeater_pathHashMode": "Modo de hash de caminho",
|
||||
"repeater_pathHashModeHelper": "Bytes utilizados para codificar o ID deste repetidor nas tags de caminho/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). As versões 1.13 e anteriores do firmware não suportam caminhos multi-byte — apenas funcionam uma vez após a ativação da rede (a partir da versão 1.14+).",
|
||||
"repeater_pathHashModeHelper": "Bytes usados para codificar o ID deste repetidor nas tags de caminho flood/detecção de loop. 0=1 byte (256 IDs, até 64 saltos), 1=2 bytes (65.000 IDs, até 32 saltos), 2=3 bytes (16 milhões de IDs, até 21 saltos). O firmware anterior à v1.14 sempre usava caminhos de 1 byte; v1.14 e versões mais recentes podem ser configuradas para caminhos de 2 ou 3 bytes.",
|
||||
"repeater_txDelay": "Atraso na entrega em Flood, TX",
|
||||
"repeater_txDelayHelper": "Ajuste de espaçamento para tráfego de inundações, como um multiplicador do tempo de transmissão (0-2, padrão 0,5). Quanto maior, menos colisões, mas uma entrega mais lenta.",
|
||||
"repeater_directTxDelay": "Atraso direto no sinal TX",
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Sem localização",
|
||||
"pathMap_followPacket": "Fixar vista no pacote",
|
||||
"pathMap_unfollowPacket": "Liberar vista do pacote",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regiões",
|
||||
"settings_regionSettingsSubtitle": "Gerenciar regiões armazenadas",
|
||||
"settings_regionManagement_screenTitle": "Gestão de Região",
|
||||
"settings_regionNameHint": "Nome da região",
|
||||
"settings_regionAddRegion": "Adicionar região",
|
||||
"settings_regionFetchRegions": "Regiões de obtenção a partir de repetidores",
|
||||
"settings_regionFetchRegionsFail": "Nenhuma região foi encontrada",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Esta região já foi adicionada",
|
||||
"settings_regionName": "Nome da Região",
|
||||
"settings_regionDeleted": "Região excluída",
|
||||
"settings_deleteRegion": "Excluir Região",
|
||||
"settings_deleteRegionConfirm": "Remover \"{region}\" da lista de regiões?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bytes",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bytes",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bytes",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35V)",
|
||||
"channels_regionSetTo": "Região: {region}",
|
||||
"channels_regionNotSet": "Região: nenhuma",
|
||||
"channels_regionSelect_Title": "Escolha uma região",
|
||||
"channels_clearRegion": "Região desimpedida",
|
||||
"chat_sendImage": "Enviar imagem",
|
||||
"chat_imagePickFailed": "Não consegui abrir essa imagem",
|
||||
"chat_receivedGif": "Recebi um GIF",
|
||||
"repeater_keySettings": "Alterar Chaves de Identidade",
|
||||
"repeater_keySettingsSubtitle": "Altere a chave pública/chave privada",
|
||||
"repeater_prvKey": "Chave privada",
|
||||
"repeater_prvKeyHelper": "Uma nova chave privada para o repetidor, uma string hexadecimal de 128 caracteres.",
|
||||
"repeater_generatePrvKey": "Gere um par de chaves aleatório",
|
||||
"repeater_stopGeneratingPrvKey": "Interrompa a busca por par de chaves",
|
||||
"repeater_pubKey": "Chave pública",
|
||||
"repeater_pubKeyHelper": "Esta é a chave pública que acompanha a chave privada gerada. Você não pode definir isso diretamente.",
|
||||
"repeater_pubKeyPrefix": "Prefixo desejado",
|
||||
"repeater_pubKeyPrefixHelper": "Encontre uma chave pública que comece com esses dígitos hexadecimais. Número esperado de tentativas: {tries}.",
|
||||
"imageMessages_enableTitle": "Mensagens de imagem",
|
||||
"imageMessages_enableSubtitle": "Envie as imagens pela malha. Requer o download de um modelo de imagem único.",
|
||||
"imageMessages_modelSectionTitle": "Modelo de imagem",
|
||||
"imageMessages_downloadModel": "Download",
|
||||
"imageMessages_cancelDownload": "Cancelar",
|
||||
"imageMessages_removeModel": "Remover modelo",
|
||||
"imageMessages_modelReady": "Pronto",
|
||||
"imageMessages_modelNotPublished": "Ainda não publicado — esta versão não pode baixá-lo.",
|
||||
"imageMessages_downloadFailed": "O modelo de imagem não pôde ser baixado.",
|
||||
"imageMessages_autoProcessTitle": "Processar imagens automaticamente",
|
||||
"imageMessages_autoProcessSubtitle": "Reconstrua cada imagem assim que ela chegar. Utiliza cerca de 2 GB de memória por segundo; dispense a reconstrução com um toque.",
|
||||
"imageSend_title": "Enviar imagem",
|
||||
"imageSend_cropNote": "Redimensionado para 512 × 512 · a proporção de aspecto não foi preservada",
|
||||
"imageSend_originalSize": "Texto Original",
|
||||
"imageSend_onAirSize": "Ao ar",
|
||||
"imageSend_quality": "Qualidade",
|
||||
"imageSend_qualityStandard": "Padrão",
|
||||
"imageSend_qualityHigh": "Alto",
|
||||
"imageSend_packetsLabel": "Pacotes",
|
||||
"imageSend_airtimeLabel": "Tempo em transmissão",
|
||||
"imageSend_sizeLabel": "Carga útil",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{pacote} other{pacotes}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Configurações de rádio desconhecidas",
|
||||
"imageSend_radioUnknownBody": "Conecte-se a um dispositivo para que o horário de transmissão possa ser calculado.",
|
||||
"imageSend_longSendTitle": "Transmissão longa",
|
||||
"imageSend_longSendBody": "Isso manterá o canal por cerca de {duration}.",
|
||||
"imageSend_floodNote": "Roteamento de inundação: cada repetidor na faixa retransmite cada pacote, fazendo com que o canal permaneça ocupado por mais tempo do que isso.",
|
||||
"imageSend_parityTitle": "Pacote de recuperação",
|
||||
"imageSend_paritySubtitle": "Um pacote extra. As mensagens em grupo não são reconhecidas, assim permitindo que o destinatário reconstrua a imagem caso um pacote seja perdido.",
|
||||
"imageSend_send": "Enviar",
|
||||
"imageSend_cancel": "Cancelar",
|
||||
"imageSend_encodeFailed": "Esta imagem não pode ser codificada.",
|
||||
"imageSend_codecDownloading": "O modelo de imagem ainda está baixando.",
|
||||
"imageSend_codecUnavailable": "O envio de imagem não está disponível neste dispositivo.",
|
||||
"imageSend_codecDisabled": "As mensagens de imagem estão desativadas nas configurações.",
|
||||
"imageSend_deviceUnsupported": "Este rádio não pode enviar pacotes de imagem. Conecte um dispositivo com firmware companheiro versão 13 ou superior.",
|
||||
"imageSend_directMessagesUnsupported": "As imagens viajam como dados em grupo, portanto, só podem ser enviadas para um canal — não em mensagem direta.",
|
||||
"imageSend_tooLarge": "Essa imagem foi codificada em mais pacotes do que o formato de malha permite.",
|
||||
"imageSend_sentConfirmation": "Imagem enviada como {count} {count, plural, =1{pacote} other{pacotes}}.",
|
||||
"imageSend_sendFailed": "A imagem não pôde ser enviada: {error}",
|
||||
"imageSend_sendingProgress": "Envio da imagem — pacote {sent} de {total}",
|
||||
"receivedImage_senderPrefix": "Nó {prefix}",
|
||||
"receivedImage_incoming": "{received} de {total} pacotes",
|
||||
"receivedImage_queued": "Aguardando para decodificar",
|
||||
"receivedImage_tapToDecode": "Toque para decodificar",
|
||||
"receivedImage_decoding": "Reconstruindo… cerca de 1 segundo",
|
||||
"receivedImage_incomplete": "Imagem incompleta — {received} de {total} pacotes chegaram",
|
||||
"receivedImage_corrupt": "A imagem não pôde ser reconstruída",
|
||||
"receivedImage_decoderMissing": "Imagem recebida — decodificação de imagem incorreta",
|
||||
"receivedImage_evicted": "Imagem não armazenada",
|
||||
"receivedImage_retry": "Tente novamente",
|
||||
"receivedImage_decodeAgain": "Decodifique novamente",
|
||||
"receivedImage_openSettings": "Configurar",
|
||||
"receivedImage_tapToProcess": "Toque para processar",
|
||||
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pacote} other{pacotes}}",
|
||||
"imageSend_secondsValue": "{seconds} segundos",
|
||||
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
|
||||
}
|
||||
|
||||
@@ -77,6 +77,8 @@
|
||||
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Долгота",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).",
|
||||
"settings_privacyMode": "Режим конфиденциальности",
|
||||
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
|
||||
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
|
||||
@@ -190,6 +192,19 @@
|
||||
"appSettings_last6Hours": "Последние 6 часов",
|
||||
"appSettings_last24Hours": "Последние 24 часа",
|
||||
"appSettings_lastWeek": "Последнюю неделю",
|
||||
"appSettings_rasterTileSource": "Источник растровых тайлов",
|
||||
"appSettings_stadiaEndpoint": "Конечная точка Stadia",
|
||||
"appSettings_stadiaApiKey": "Ключ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Требуется для использования Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Настроено: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Введите свой ключ API Stadia Maps. Приложение использует его для запросов растровых тайлов.",
|
||||
"appSettings_offlineMapCache": "Кэш офлайн-карты",
|
||||
"appSettings_noAreaSelected": "Область не выбрана",
|
||||
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}–{maxZoom})",
|
||||
@@ -438,6 +453,56 @@
|
||||
"mapCache_downloadTilesButton": "Загрузить плитки",
|
||||
"mapCache_clearCacheButton": "Очистить кэш",
|
||||
"mapCache_failedDownloads": "Неудачных загрузок: {count}",
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
|
||||
"time_justNow": "Только что",
|
||||
"time_minutesAgo": "{minutes} мин назад",
|
||||
@@ -1478,7 +1543,7 @@
|
||||
"repeater_advancedSettings": "Продвинутый",
|
||||
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
|
||||
"repeater_pathHashMode": "Режим хеширования пути",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах для обнаружения потоков/циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (1 600 000 идентификаторов, до 21 перехода). Версии прошивки v1.13 и более ранние версии не поддерживают многобайтовые пути — они поднимаются только после того, как ваша сеть будет обновлена до версии v1.14 и выше.",
|
||||
"repeater_pathHashModeHelper": "Байты, используемые для кодирования идентификатора этого ретранслятора в тегах flood-маршрута/обнаружения циклов. 0 = 1 байт (256 идентификаторов, до 64 переходов), 1 = 2 байта (65 000 идентификаторов, до 32 переходов), 2 = 3 байта (16 миллионов идентификаторов, до 21 перехода). Прошивки до v1.14 всегда использовали 1-байтовые маршруты; v1.14 и новее можно настроить на 2- или 3-байтовые маршруты.",
|
||||
"repeater_txDelay": "Задержка в работе системы Flood TX",
|
||||
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
|
||||
"repeater_directTxDelay": "Прямая задержка сигнала TX",
|
||||
@@ -1843,5 +1908,227 @@
|
||||
"pathMap_noLocation": "Нет координат",
|
||||
"pathMap_followPacket": "Следить за пакетом",
|
||||
"pathMap_unfollowPacket": "Не следить за пакетом",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Регионы",
|
||||
"settings_regionSettingsSubtitle": "Управление сохранёнными регионами",
|
||||
"settings_regionManagement_screenTitle": "Управление регионами",
|
||||
"settings_regionNameHint": "Введите название региона",
|
||||
"settings_regionAddRegion": "Добавить регион",
|
||||
"settings_regionFetchRegions": "Извлекать регионы с ретрансляторов",
|
||||
"settings_regionFetchRegionsFail": "Не были найдены никакие регионы",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Этот регион уже был добавлен",
|
||||
"settings_regionName": "Название региона",
|
||||
"settings_regionDeleted": "Регион удалён",
|
||||
"settings_deleteRegion": "Удалить регион",
|
||||
"settings_deleteRegionConfirm": "Удалить \"{region}\" из списка регионов?",
|
||||
"settings_infoHardware": "Оборудование",
|
||||
"settings_infoFirmware": "Производственное программное обеспечение",
|
||||
"repeater_pathHashModeOption0": "0 - 1 байт",
|
||||
"repeater_pathHashModeOption1": "1 - 2 байта",
|
||||
"repeater_pathHashModeOption2": "2 - 3 байта",
|
||||
"repeater_pathHashModeOption3": "3 - 4 байта",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0–4,35 В)",
|
||||
"channels_regionSetTo": "РЕГИОН: {region}",
|
||||
"channels_regionNotSet": "Регион: отсутствует",
|
||||
"channels_regionSelect_Title": "Выберите регион",
|
||||
"channels_clearRegion": "Чистый регион",
|
||||
"chat_sendImage": "Отправьте изображение",
|
||||
"chat_imagePickFailed": "Не удалось открыть это изображение",
|
||||
"chat_receivedGif": "Получен GIF",
|
||||
"repeater_keySettings": "Изменить ключи идентичности",
|
||||
"repeater_keySettingsSubtitle": "Измените пару публичных/частных ключей",
|
||||
"repeater_prvKey": "Личный ключ",
|
||||
"repeater_prvKeyHelper": "Для повторителя генерируется новый приватный ключ — шестнадцатеричная строка из 128 символов.",
|
||||
"repeater_generatePrvKey": "Сгенерируйте случайный ключ-пару",
|
||||
"repeater_stopGeneratingPrvKey": "Прервать поиск ключной пары",
|
||||
"repeater_pubKey": "Публичный ключ",
|
||||
"repeater_pubKeyHelper": "Это публичный ключ, который соответствует сгенерированному приватному ключу. Вы не можете установить его напрямую.",
|
||||
"repeater_pubKeyPrefix": "Желаемый префикс",
|
||||
"repeater_pubKeyPrefixHelper": "Найдите публичный ключ, начинающийся этими шестнадцатеричными цифрами. Ожидаемое количество попыток: {tries}.",
|
||||
"imageMessages_enableTitle": "Включить изображения в сообщения",
|
||||
"imageMessages_enableSubtitle": "Отправьте изображения через сеть. Требуется однократная загрузка шаблона изображения.",
|
||||
"imageMessages_modelSectionTitle": "Модель изображения",
|
||||
"imageMessages_downloadModel": "Скачать",
|
||||
"imageMessages_cancelDownload": "Отмена",
|
||||
"imageMessages_removeModel": "Удалить модель",
|
||||
"imageMessages_modelReady": "Готов",
|
||||
"imageMessages_modelNotPublished": "Пока не опубликовано — эта сборка не может скачать это.",
|
||||
"imageMessages_downloadFailed": "Модель изображения не удалось скачать.",
|
||||
"imageMessages_autoProcessTitle": "Автоматически обрабатывать изображения",
|
||||
"imageMessages_autoProcessSubtitle": "Примерно каждое изображение реконструируйте сразу после его поступления. Для этого на каждый раз используется около 2 ГБ памяти; отключите реконструкцию касанием.",
|
||||
"imageSend_title": "Отправьте изображение",
|
||||
"imageSend_cropNote": "Меняется размер на 512 × 512 · соотношение сторон не сохраняется",
|
||||
"imageSend_originalSize": "Оригинал",
|
||||
"imageSend_onAirSize": "В эфире",
|
||||
"imageSend_quality": "Качество",
|
||||
"imageSend_qualityStandard": "Стандарт",
|
||||
"imageSend_qualityHigh": "Высота",
|
||||
"imageSend_packetsLabel": "Пакеты",
|
||||
"imageSend_airtimeLabel": "Время на эфире",
|
||||
"imageSend_sizeLabel": "Пакет",
|
||||
"imageSend_packetsCount": "{count} {count, plural, one{пакет} few{пакета} many{пакетов} other{пакета}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Настройки радио неизвестны",
|
||||
"imageSend_radioUnknownBody": "Подключитесь к устройству, чтобы можно было рассчитать время эфирного времени.",
|
||||
"imageSend_longSendTitle": "Долгая передача",
|
||||
"imageSend_longSendBody": "Это будет поддерживать канал примерно {duration}.",
|
||||
"imageSend_floodNote": "Руководство по маршрутизации потоков: каждый ретрансатор в зоне перераспределяет каждый пакет, поэтому канал остаётся занятным дольше, чем это.",
|
||||
"imageSend_parityTitle": "Пакет восстановления",
|
||||
"imageSend_paritySubtitle": "Один дополнительный пакет. Групповые сообщения не подтверждаются, поэтому это позволяет получателю восстановить изображение, если один пакет потерян.",
|
||||
"imageSend_send": "Отправить",
|
||||
"imageSend_cancel": "Отмена",
|
||||
"imageSend_encodeFailed": "Это изображение не может быть закодировано.",
|
||||
"imageSend_codecDownloading": "Модель изображения всё ещё загружается.",
|
||||
"imageSend_codecUnavailable": "Отправка изображения на этом устройстве недоступна.",
|
||||
"imageSend_codecDisabled": "В настройках отключены сообщения с изображениями.",
|
||||
"imageSend_deviceUnsupported": "Это радио не может отправлять пакеты изображений. Подключите устройство, работающее на прошивке-компаньоне версии 13 или выше.",
|
||||
"imageSend_directMessagesUnsupported": "Изображения передаются как групповые данные, поэтому их можно отправлять только в канал, а не в личное сообщение.",
|
||||
"imageSend_tooLarge": "Изображение было закодировано в больше пакетов, чем позволяет формат сетки.",
|
||||
"imageSend_sentConfirmation": "Изображение отправлено как {count} {count, plural, =1{пакет} other{пакетов}}.",
|
||||
"imageSend_sendFailed": "Изображение не может быть отправлено: {error}",
|
||||
"imageSend_sendingProgress": "Отправка изображения — пакет {sent} из {total}",
|
||||
"receivedImage_senderPrefix": "Узел {prefix}",
|
||||
"receivedImage_incoming": "{received} из {total} пакетов",
|
||||
"receivedImage_queued": "Ожидание расшифровки",
|
||||
"receivedImage_tapToDecode": "Нажмите для декодирования",
|
||||
"receivedImage_decoding": "Реконструкция… примерно за 1 секунду",
|
||||
"receivedImage_incomplete": "Отсутствует изображение — {received} из {total} пакетов пришло",
|
||||
"receivedImage_corrupt": "Изображение не может быть восстановлено",
|
||||
"receivedImage_decoderMissing": "Получен изображение — декодирование изображения неправильное",
|
||||
"receivedImage_evicted": "Изображение больше не сохраняется",
|
||||
"receivedImage_retry": "Попробуйте снова",
|
||||
"receivedImage_decodeAgain": "Декодируйте снова",
|
||||
"receivedImage_openSettings": "Настройте",
|
||||
"receivedImage_tapToProcess": "Нажмите для обработки",
|
||||
"receivedImage_awaiting": "{bytes} байт · {packets} {packets, plural, =1{пакет} other{пакетов}}",
|
||||
"imageSend_secondsValue": "{seconds} секунд",
|
||||
"imageSend_minutesSecondsValue": "{minutes} м {seconds} с"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Neplatná šírka alebo dĺžka.",
|
||||
"settings_latitude": "Súradnica",
|
||||
"settings_longitude": "Dĺžka",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatický zero-hop inzerát pri aktualizácii GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Keď sa GPS poloha zmení, odoslať zero-hop inzerát (vyžaduje polohu v inzeráte).",
|
||||
"settings_privacyMode": "Režim ochrany súkromia",
|
||||
"settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách",
|
||||
"settings_privacyModeToggle": "Prepínač súkromného režimu skryje vaše meno a polohu v reklamách.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Posledné 6 hodín",
|
||||
"appSettings_last24Hours": "Posledných 24 hodín",
|
||||
"appSettings_lastWeek": "Minul týždeň",
|
||||
"appSettings_rasterTileSource": "Zdroj rastrových dlaždíc",
|
||||
"appSettings_stadiaEndpoint": "Koncový bod Stadia",
|
||||
"appSettings_stadiaApiKey": "Kľúč API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Vyžaduje sa na používanie Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Nakonfigurované: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Zadajte svoj kľúč API pre Stadia Maps. Aplikácia ho používa na požiadavky na rastrové dlaždice.",
|
||||
"appSettings_offlineMapCache": "Offline Mapa Pamäť",
|
||||
"appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť",
|
||||
"appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1107,7 +1172,7 @@
|
||||
"repeater_advancedSettings": "Pokročilé",
|
||||
"repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov",
|
||||
"repeater_pathHashMode": "Režim hashovania cesty",
|
||||
"repeater_pathHashModeHelper": "Byty použité na zakódovanie ID tohto opakovača v tagoch pre trasu/detekciu slučky. 0 = 1 bytu (256 ID, až 64 skokov), 1 = 2 byty (65 000 ID, až 32 skokov), 2 = 3 byty (16 miliónov ID, až 21 skokov). Verzie 1.13 a staršie nepodporujú viacbytové trasy – fungujú len, keď je sieť aktivovaná.",
|
||||
"repeater_pathHashModeHelper": "Bajty použité na zakódovanie ID tohto opakovača v tagoch flood trasy/detekcie slučky. 0=1 bajt (256 ID, až 64 skokov), 1=2 bajty (65 000 ID, až 32 skokov), 2=3 bajty (16 miliónov ID, až 21 skokov). Firmvér pred v1.14 vždy používal 1-bajtové trasy; v1.14 a novšie možno nakonfigurovať na 2- alebo 3-bajtové trasy.",
|
||||
"repeater_txDelay": "Zpoždanie v Flood, TX",
|
||||
"repeater_txDelayHelper": "Nastavenie pre opakované vysielanie pre dopravu počas povodní, ako násobok času, ktorý paket využije (0-2, výchoce hodnota 0,5). Vyššia hodnota znamená menej kolízii, ale pomalšie doručovanie.",
|
||||
"repeater_directTxDelay": "Priame oneskorenie TX",
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Bez polohy",
|
||||
"pathMap_followPacket": "Uzamknúť pohľad na paket",
|
||||
"pathMap_unfollowPacket": "Odomknúť pohľad od paketu",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regiony",
|
||||
"settings_regionSettingsSubtitle": "Zohľať uložené regiony",
|
||||
"settings_regionManagement_screenTitle": "Regiónne vedenie",
|
||||
"settings_regionNameHint": "Vstuptejte název regionu",
|
||||
"settings_regionAddRegion": "Dodajte region",
|
||||
"settings_regionFetchRegions": "Odberanie regionov z repeaterov",
|
||||
"settings_regionFetchRegionsFail": "Nebylo našli žiadné regiony",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Tento oblast je už dodán",
|
||||
"settings_regionName": "Regionné název",
|
||||
"settings_regionDeleted": "Regionu zníteno",
|
||||
"settings_deleteRegion": "Vyniknúcia regionu",
|
||||
"settings_deleteRegionConfirm": "Odstraniť {region} z listy regionov?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Firmware",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byt",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bity",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bity",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bity",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"channels_regionNotSet": "Region: žiadné",
|
||||
"channels_regionSelect_Title": "Vyberte region",
|
||||
"channels_clearRegion": "Jasný údajný oblasť",
|
||||
"chat_sendImage": "Vyslat obraz",
|
||||
"chat_imagePickFailed": "Nesmiela jsem otvoriť tento obrázok",
|
||||
"chat_receivedGif": "Odeslaný GIF",
|
||||
"repeater_keySettings": "Zmena identititačných klíčov",
|
||||
"repeater_keySettingsSubtitle": "Zmena prehľadného/osobného klíčového zbraň",
|
||||
"repeater_prvKey": "Prívatná klíč",
|
||||
"repeater_prvKeyHelper": "Nová prívna klíčová číslo pre repeater, 128-znáčlový šestnáctorýč.",
|
||||
"repeater_generatePrvKey": "Vytvorenie príma kľúčového paru",
|
||||
"repeater_stopGeneratingPrvKey": "Prerušenie hľadania klíčového paru",
|
||||
"repeater_pubKey": "Public key",
|
||||
"repeater_pubKeyHelper": "Táto je veľká klíč, ktorý sa vztahuje s generovaným manéčným klíčom. Nemožné je to ustanovit priateľne.",
|
||||
"repeater_pubKeyPrefix": "Požadovaný prefiks",
|
||||
"repeater_pubKeyPrefixHelper": "Zahľadnite sa veľkú publicenu klíču, ktorá začíná tými šestícih štiastkami. Očekivané potrebné pokusy: {tries}.",
|
||||
"imageMessages_enableTitle": "Zabezpečte zobrazi",
|
||||
"imageMessages_enableSubtitle": "Odeslanie obrazov pre mšťanie. Potrebujete jednorazové stáhnutie modelu obrazov.",
|
||||
"imageMessages_modelSectionTitle": "Model v kínajme",
|
||||
"imageMessages_downloadModel": "Stáhnie",
|
||||
"imageMessages_cancelDownload": "Zastavenie",
|
||||
"imageMessages_removeModel": "Vyhniť model",
|
||||
"imageMessages_modelReady": "Čiačný",
|
||||
"imageMessages_modelNotPublished": "Nesprávne vypisáné — tento prechovka nie môže jej stáhnuť.",
|
||||
"imageMessages_downloadFailed": "Model s obrázkom nemôžno sťažovať.",
|
||||
"imageMessages_autoProcessTitle": "Automaticky prepojiť slučaje.",
|
||||
"imageMessages_autoProcessSubtitle": "Rekonstruujte každý obraz tak, ako pridá. Používajete okolo 2 GB pamäti pre drugú hodinu; odložte rekonstrukciu tým kliknutím.",
|
||||
"imageSend_title": "Vyslat obraz",
|
||||
"imageSend_cropNote": "Zmeneno na 512 × 512 · vzorýst nie byl zachovan",
|
||||
"imageSend_originalSize": "Výborný text:\n\n\"Language is the road map of a culture. It tells us where to go as a society and helps us understand the world around us. Learning a new language opens up new perspectives and opportunities, allowing us to connect with people from different backgrounds and build bridges between cultures.\"\n\nSlovenská preklada:\n\n\"Jazyk je cesta pre zručnosti a odráža kultúru. Dá nám uvedomi, dokádať, kúdy sa pohladneme a pomáha našej společnosti poradiť sa v rôznych okolnosti a porozumieť o svetu kolem nás. Nauka nového jazyka otviera nové perspektívy a možnosti, čo pohybuje našu schopnosť sa spoluprávať s ľudmi z rôznych okolnosti a budovať mosty medzi kultúry.\"",
|
||||
"imageSend_onAirSize": "V prúce",
|
||||
"imageSend_quality": "Kvalita",
|
||||
"imageSend_qualityStandard": "Stándard",
|
||||
"imageSend_qualityHigh": "Vysoká",
|
||||
"imageSend_packetsLabel": "Pakety",
|
||||
"imageSend_airtimeLabel": "Čas na anteni",
|
||||
"imageSend_sizeLabel": "Základ",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paketov}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Nastavenia radio nezná",
|
||||
"imageSend_radioUnknownBody": "Zkonečte s určým prijemcem, aby sa mohlo výpočet času, ktorý je v prúbežu.",
|
||||
"imageSend_longSendTitle": "Dlouhá transmisia",
|
||||
"imageSend_longSendBody": "Tento dôvod udrží kanál priamo na {duration}.",
|
||||
"imageSend_floodNote": "Povodný routing: každý repeater v rozsahu zrňa každý paket, takže kanál zostá zajetší dlouше, než tu.",
|
||||
"imageSend_parityTitle": "Poučný paket pre očudenie",
|
||||
"imageSend_paritySubtitle": "Jedný dodatný paket. Skúsené súhlasy nie sa uznávaú, takže to umožňuje prechovávaču opraviť obraz, pokiaľ sa jedný paket niebyl ztrácený.",
|
||||
"imageSend_send": "Odeslať",
|
||||
"imageSend_cancel": "Zastavenie",
|
||||
"imageSend_encodeFailed": "Tento obráz nie byla byla schována.",
|
||||
"imageSend_codecDownloading": "Model prehľadovania prebeža stále stále stále prechádza.",
|
||||
"imageSend_codecUnavailable": "Zobrazenie nie je dostupný na tomto zariadení.",
|
||||
"imageSend_codecDisabled": "Zobrazy s prešteľkami sú v nastavení vypnené.",
|
||||
"imageSend_deviceUnsupported": "Tento radiok nie môže odeslať obrazové pakety. Podeďte správne určité zařízení s oprogramovaním spolupráce 13 alebo novším.",
|
||||
"imageSend_directMessagesUnsupported": "Obrazy sa prechádzajú jako skupinové údaje, preto sa ich môžu odeslať len na kanál — nie v príbeh sa.",
|
||||
"imageSend_tooLarge": "Tento obraz byl skodyrovany do viac paketov, ako umožňuje si sítka format.",
|
||||
"imageSend_sentConfirmation": "Obrázok odoslaný ako {count} {count, plural, =1{paket} other{paketov}}.",
|
||||
"imageSend_sendFailed": "Obraz nemôžlo odeslať: {error}",
|
||||
"imageSend_sendingProgress": "Vysyťanie obrazu — pakét {sent} z {total}",
|
||||
"receivedImage_senderPrefix": "Vęzlo {prefix}",
|
||||
"receivedImage_incoming": "Otrzymané {received} z {total} pakotí",
|
||||
"receivedImage_queued": "Čekanie na dekódovanie",
|
||||
"receivedImage_tapToDecode": "Kliknite, aby dekódovať",
|
||||
"receivedImage_decoding": "Rekonstrukcia… o čoľve 1 s",
|
||||
"receivedImage_incomplete": "Obraz nieješťajný — {received} z {total} paciek príšlo",
|
||||
"receivedImage_corrupt": "Obraz nemôžu byť prepojený",
|
||||
"receivedImage_decoderMissing": "Otrzymaná slúba — dekódovanie slúby je vypadejce",
|
||||
"receivedImage_evicted": "Obraz nemá uložený",
|
||||
"receivedImage_retry": "Zkôr to opítajte",
|
||||
"receivedImage_decodeAgain": "Zdeľte sa znovu",
|
||||
"receivedImage_openSettings": "Založte",
|
||||
"receivedImage_tapToProcess": "Ukliknite, aby sa príjomili",
|
||||
"receivedImage_awaiting": "{bytes} bajtov · {packets} {packets, plural, =1{paket} other{paketov}}",
|
||||
"imageSend_secondsValue": "{seconds} s",
|
||||
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
|
||||
"settings_latitude": "Širina",
|
||||
"settings_longitude": "Dolžina",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Samodejni zero-hop oglas ob posodobitvi GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ko se GPS lokacija spremeni, pošlji zero-hop oglas (zahteva lokacijo v oglasu).",
|
||||
"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.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "Zadnjih 6 ur",
|
||||
"appSettings_last24Hours": "Zadnjih 24 ur",
|
||||
"appSettings_lastWeek": "Prejšnji teden",
|
||||
"appSettings_rasterTileSource": "Vir rastrskih ploščic",
|
||||
"appSettings_stadiaEndpoint": "Končna točka Stadia",
|
||||
"appSettings_stadiaApiKey": "Ključ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Obvezno za uporabo Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Nastavljeno: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Vnesite svoj ključ API za Stadia Maps. Aplikacija ga uporablja za zahteve rastrskih ploščic.",
|
||||
"appSettings_offlineMapCache": "Shramba zemljevidov brez povezave",
|
||||
"appSettings_noAreaSelected": "Območje ni izbrano",
|
||||
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1107,7 +1172,7 @@
|
||||
"repeater_advancedSettings": "Napredno",
|
||||
"repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike",
|
||||
"repeater_pathHashMode": "Način ustvarjanja hash-a poti",
|
||||
"repeater_pathHashModeHelper": "Biti, ki so bila uporabljena za kodiranje ID-ja tega releja v oznakah za zaznavanje pot/kroga, imajo naslednje velikosti: 0=1 bit (256 ID-jev, do 64 skokov), 1=2 biti (65.000 ID-jev, do 32 skokov), 2=3 biti (16 milijonov ID-jev, do 21 skokov). V različicah 1.13 in starejših se ustvarjajo večbitne poti – vendar se to zgodi šele, ko je omrežje vklopljeno v različicah 1.14 in kasnejših.",
|
||||
"repeater_pathHashModeHelper": "Bajti, uporabljeni za kodiranje ID-ja tega repetitorja v oznakah flood poti/zaznavanja zank. 0=1 bajt (256 ID-jev, do 64 skokov), 1=2 bajta (65.000 ID-jev, do 32 skokov), 2=3 bajti (16 milijonov ID-jev, do 21 skokov). Vdelana programska oprema pred v1.14 je vedno uporabljala 1-bajtne poti; v1.14 in novejše je mogoče nastaviti na 2- ali 3-bajtne poti.",
|
||||
"repeater_txDelay": "Zatemnitevanje zaradi poplav v Texasu",
|
||||
"repeater_txDelayHelper": "Uporaba intervalov za ponovno pošiljanje v primeru prometa zaradi poplav, kot pomnožnik časovne trajanje paketa (0-2, privzeto 0,5). Veje vrednost = manjše kolizije, vendar počasnejše dostavo.",
|
||||
"repeater_directTxDelay": "Neposredni časovno odlašanje",
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Brez lokacije",
|
||||
"pathMap_followPacket": "Zakleni pogled na paket",
|
||||
"pathMap_unfollowPacket": "Odkleni pogled od paketa",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regioni",
|
||||
"settings_regionSettingsSubtitle": "Upravljanje opisanih regionov",
|
||||
"settings_regionManagement_screenTitle": "Regijonsko upravljanje",
|
||||
"settings_regionNameHint": "Naslov občine",
|
||||
"settings_regionAddRegion": "Dodaj region",
|
||||
"settings_regionFetchRegions": "Pobražujte izvajne področja iz ponavljalnikov",
|
||||
"settings_regionFetchRegionsFail": "Nižnih občin ni izkrali",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Taj oblasi je več pomanjkan.",
|
||||
"settings_regionName": "Naziv Regiona",
|
||||
"settings_regionDeleted": "Oblikačena regija",
|
||||
"settings_deleteRegion": "Prihoda Region",
|
||||
"settings_deleteRegionConfirm": "Ostani {region} iz listy regionov?",
|
||||
"settings_infoHardware": "Hardver",
|
||||
"settings_infoFirmware": "Pravilnik",
|
||||
"repeater_pathHashModeOption0": "0 - 1 bajt",
|
||||
"repeater_pathHashModeOption1": "1 - 2 bite",
|
||||
"repeater_pathHashModeOption2": "2 - 3 bite",
|
||||
"repeater_pathHashModeOption3": "3 - 4 bite",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0–4,35 V)",
|
||||
"channels_regionSetTo": "Osebno: {region}",
|
||||
"channels_regionNotSet": "Region: brak",
|
||||
"channels_regionSelect_Title": "Vablajte region",
|
||||
"channels_clearRegion": "Jasna regija",
|
||||
"chat_sendImage": "Sločaji slik",
|
||||
"chat_imagePickFailed": "Nispeval sem tega ta slik",
|
||||
"chat_receivedGif": "Otrvljen GIF",
|
||||
"repeater_keySettings": "Zmeničke ključe identitete",
|
||||
"repeater_keySettingsSubtitle": "Zamenjajte publican ali privatni ključni par",
|
||||
"repeater_prvKey": "Privarna ključa",
|
||||
"repeater_prvKeyHelper": "Nova privatna ključa za prekaznik, 128- znakovni šestnastik.",
|
||||
"repeater_generatePrvKey": "Sporočna ključna para",
|
||||
"repeater_stopGeneratingPrvKey": "Prekajanje odkrivanja ključne paro",
|
||||
"repeater_pubKey": "Public key (publična ključa)",
|
||||
"repeater_pubKeyHelper": "Taj je publicna ključ, ki pomeni pripovedno privatno ključ. Ne možeš to vrediti direktno.",
|
||||
"repeater_pubKeyPrefix": "Poželana prefiks",
|
||||
"repeater_pubKeyPrefixHelper": "Najdi publicen ključ, ki začne s tih hexnih čitov. Očekivani pokusi: {tries}.",
|
||||
"imageMessages_enableTitle": "Pozivni izkričile slike",
|
||||
"imageMessages_enableSubtitle": "Prešli vzorce pre mrežo. Potrebno je jednokratno stworiti model vzorca.",
|
||||
"imageMessages_modelSectionTitle": "Obrazni model",
|
||||
"imageMessages_downloadModel": "Storba",
|
||||
"imageMessages_cancelDownload": "Odločite",
|
||||
"imageMessages_removeModel": "Ubrani model",
|
||||
"imageMessages_modelReady": "Gotov",
|
||||
"imageMessages_modelNotPublished": "Jo še niso izdana — ta izdelka te ne može prebaviti.",
|
||||
"imageMessages_downloadFailed": "Obrazni model ne može biti preuzet.",
|
||||
"imageMessages_autoProcessTitle": "Avtomatizirane slike",
|
||||
"imageMessages_autoProcessSubtitle": "Katero je oblikovanje svega oblikov, ko se pošteva. Za drugo okolo časa se koristi ok. 2 GB pamične prostore; odločite se za oblikovanje s tukom.",
|
||||
"imageSend_title": "Sločaji slik",
|
||||
"imageSend_cropNote": "Zadovoljno izmenjeno na 512 × 512 · vzorovni odnos ne ohranjen",
|
||||
"imageSend_originalSize": "Original text:\n\nThe early bird catches the worm.\n\nTranslation:\n\nPojdi prvi, zlati v kroku.",
|
||||
"imageSend_onAirSize": "Na vodi",
|
||||
"imageSend_quality": "Kakovost",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Vysoko",
|
||||
"imageSend_packetsLabel": "Pakete",
|
||||
"imageSend_airtimeLabel": "Čas v eteru",
|
||||
"imageSend_sizeLabel": "Napravo",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paketov}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Naslušniki radio neznane",
|
||||
"imageSend_radioUnknownBody": "Poučajte se na stroj, da se lahko izračunuje čas v prikazu.",
|
||||
"imageSend_longSendTitle": "Dluga preposlana",
|
||||
"imageSend_longSendBody": "Ovo bo ohranilo kanal prihodno za obliko {duration}.",
|
||||
"imageSend_floodNote": "Povlatni potok: sve repeaterje v območju pretrasirajo svak paket, pa kanál ostane zanudnejši od tega.",
|
||||
"imageSend_parityTitle": "Pobavni paket zdržbe",
|
||||
"imageSend_paritySubtitle": "Jedna dodatna paczka. Grupni izjasi ne izvajajo potvarnosti, pa ta izdelka omogoča preuzimanju ponovno izgleda, če je zgubjen pojedni paket.",
|
||||
"imageSend_send": "Posyłaj",
|
||||
"imageSend_cancel": "Odločite",
|
||||
"imageSend_encodeFailed": "Ovo slikovo ne more biti zakodirano.",
|
||||
"imageSend_codecDownloading": "Obrazni model še pospeva preborjanje.",
|
||||
"imageSend_codecUnavailable": "Poslanje slike nema na tem zelo naročnem naročnem.",
|
||||
"imageSend_codecDisabled": "Obrazi s pomanjkanjem so v nastavnih.",
|
||||
"imageSend_deviceUnsupported": "Taj radio ne može posredovati slike pakuete. Prepoštijo izbirni napravnik s kompanijskimi programi na čevem 13 ali novšejšim.",
|
||||
"imageSend_directMessagesUnsupported": "Obrazy se pojavijo kot skupnih podatkov, pa lahko ih poslujejo samo na kanal — ne v prilogih.",
|
||||
"imageSend_tooLarge": "Taj slik je zakodiran v viših pakotnih od športne formate, ki omogoča mrežni format.",
|
||||
"imageSend_sentConfirmation": "Slika poslana kot {count} {count, plural, =1{paket} other{paketov}}.",
|
||||
"imageSend_sendFailed": "Obraz ne može biti poslany: {error}",
|
||||
"imageSend_sendingProgress": "Prekazivanje izkaza — paket {sent} od {total}",
|
||||
"receivedImage_senderPrefix": "Vrtitev {prefix}",
|
||||
"receivedImage_incoming": "Otrteno od {received} na {total} poslovanih potokov",
|
||||
"receivedImage_queued": "Čekanje, da se dekodira",
|
||||
"receivedImage_tapToDecode": "Kliknite, da razkrivete",
|
||||
"receivedImage_decoding": "Ostvarjanje… o prihodnosti za okolo 1 s",
|
||||
"receivedImage_incomplete": "Obraz nije kompletan — {received} od {total} poslaničev je prishodil",
|
||||
"receivedImage_corrupt": "Obraz ne more lahko rekonstruirati",
|
||||
"receivedImage_decoderMissing": "Otrgena slika — dekodirana slika je izključena",
|
||||
"receivedImage_evicted": "Obraz ni več niso zakajiren",
|
||||
"receivedImage_retry": "Pokušajte znova",
|
||||
"receivedImage_decodeAgain": "Dekodiraj ponovno",
|
||||
"receivedImage_openSettings": "Nastavite",
|
||||
"receivedImage_tapToProcess": "Kliknij za obravnavanje",
|
||||
"receivedImage_awaiting": "{bytes} bajtov · {packets} {packets, plural, =1{paket} other{paketov}}",
|
||||
"imageSend_secondsValue": "{seconds} s",
|
||||
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s"
|
||||
}
|
||||
|
||||
@@ -100,6 +100,8 @@
|
||||
"settings_locationInvalid": "Ogiltig latitud eller longitud.",
|
||||
"settings_latitude": "Latitud",
|
||||
"settings_longitude": "Längdgrad",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatisk zero-hop-annons vid GPS-uppdatering",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "När GPS-positionen ändras, skicka en zero-hop-annons (kräver plats i annonsen).",
|
||||
"settings_privacyMode": "Privatläge",
|
||||
"settings_privacyModeSubtitle": "Dölj namn/plats i annonser",
|
||||
"settings_privacyModeToggle": "Aktivera privatläge för att dölja ditt namn och din plats i annonser.",
|
||||
@@ -240,6 +242,19 @@
|
||||
"appSettings_last6Hours": "De senaste 6 timmarna",
|
||||
"appSettings_last24Hours": "De senaste 24 timmarna",
|
||||
"appSettings_lastWeek": "Förra veckan",
|
||||
"appSettings_rasterTileSource": "Källa för rasterplattor",
|
||||
"appSettings_stadiaEndpoint": "Stadia-slutpunkt",
|
||||
"appSettings_stadiaApiKey": "Stadia API-nyckel",
|
||||
"appSettings_stadiaApiKeyRequired": "Krävs för att använda Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Konfigurerad: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Ange din Stadia Maps API-nyckel. Appen använder den för förfrågningar om rasterplattor.",
|
||||
"appSettings_offlineMapCache": "Offline Kartcache",
|
||||
"appSettings_noAreaSelected": "Ingen area markerad",
|
||||
"appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})",
|
||||
@@ -769,6 +784,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1107,7 +1172,7 @@
|
||||
"repeater_advancedSettings": "Avancerad",
|
||||
"repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare",
|
||||
"repeater_pathHashMode": "Hash-läge för sökväg",
|
||||
"repeater_pathHashModeHelper": "Byte används för att koda denna repeaters ID i taggar för att upptäcka loopar/flödesvägar. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Versioner 1.13 och äldre har stöd för multi-byte-vägar – endast en gång när nätverket är aktiverat (från och med version 1.14).",
|
||||
"repeater_pathHashModeHelper": "Byte som används för att koda denna repeaters ID i taggar för flood-väg/loopdetektering. 0=1 byte (256 ID:n, upp till 64 hopp), 1=2 byte (65 000 ID:n, upp till 32 hopp), 2=3 byte (16 miljoner ID:n, upp till 21 hopp). Firmware före v1.14 använde alltid 1-byte-vägar; v1.14 och nyare kan konfigureras för 2- eller 3-byte-vägar.",
|
||||
"repeater_txDelay": "Försening i Flood TX",
|
||||
"repeater_txDelayHelper": "Återöverföringsintervall för trafik under perioder med hög belastning, som en multiplikator av paketets överföringstid (0-2, standard 0,5). Högre värde = färre kollisioner, men långsammare leverans.",
|
||||
"repeater_directTxDelay": "Direkt TX-fördröjning",
|
||||
@@ -2540,5 +2605,227 @@
|
||||
"pathMap_noLocation": "Ingen position",
|
||||
"pathMap_unfollowPacket": "Lås upp vy från paket",
|
||||
"pathMap_followPacket": "Lås vy till paket",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Regioner",
|
||||
"settings_regionSettingsSubtitle": "Hämtade regioner",
|
||||
"settings_regionManagement_screenTitle": "Regionförvaltning",
|
||||
"settings_regionNameHint": "Enter regionnamn",
|
||||
"settings_regionAddRegion": "Lag till region",
|
||||
"settings_regionFetchRegions": "Hämtar regioner från repeater",
|
||||
"settings_regionFetchRegionsFail": "Ingen region hittades",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Denna region har redan lagt till",
|
||||
"settings_regionName": "Regionnavn",
|
||||
"settings_regionDeleted": "Regionens kallt offentlig",
|
||||
"settings_deleteRegion": "Förhålla regio",
|
||||
"settings_deleteRegionConfirm": "Fjärna \"{region}\" ur regionlistan?",
|
||||
"settings_infoHardware": "Hardware",
|
||||
"settings_infoFirmware": "Föredata",
|
||||
"repeater_pathHashModeOption0": "0 - 1 byte",
|
||||
"repeater_pathHashModeOption1": "1 - 2 byte",
|
||||
"repeater_pathHashModeOption2": "2 - 3 byte",
|
||||
"repeater_pathHashModeOption3": "3 - 4 byte",
|
||||
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
|
||||
"channels_regionSetTo": "Region: {region}",
|
||||
"channels_regionNotSet": "Region: ingen",
|
||||
"channels_regionSelect_Title": "Välj en region",
|
||||
"channels_clearRegion": "klar region",
|
||||
"chat_sendImage": "Send vagnbild",
|
||||
"chat_imagePickFailed": "Kunde inte öppna det bildet",
|
||||
"chat_receivedGif": "Mottagit en GIF",
|
||||
"repeater_keySettings": "Ändra identitetsnyckel",
|
||||
"repeater_keySettingsSubtitle": "Vänd nya offentliga/privata nyckelpar",
|
||||
"repeater_prvKey": "Privat nyckel",
|
||||
"repeater_prvKeyHelper": "En ny privat nyckel för repeatern, en hexsträng med 128 tecken.",
|
||||
"repeater_generatePrvKey": "Generera en kära nyckelpaar",
|
||||
"repeater_stopGeneratingPrvKey": "Avbröka sökande efter nyckelpa",
|
||||
"repeater_pubKey": "Öffentlig nyckel",
|
||||
"repeater_pubKeyHelper": "Detta är den offentliga nyckeln som tillhör genererad privaten nyckel. Du kan inte setta den direkt.",
|
||||
"repeater_pubKeyPrefix": "Önskad prefix",
|
||||
"repeater_pubKeyPrefixHelper": "Hitta en offentlig nyckel som börjar med dessa hexadecimala siffror. Förväntad antal försök: {tries}.",
|
||||
"imageMessages_enableTitle": "Bildermeddelanden",
|
||||
"imageMessages_enableSubtitle": "Send bilder över mönstret. Det kräver en enkelt bildmodellhämtning.",
|
||||
"imageMessages_modelSectionTitle": "Bildmodell",
|
||||
"imageMessages_downloadModel": "Download",
|
||||
"imageMessages_cancelDownload": "Avbud",
|
||||
"imageMessages_removeModel": "Förhåll dig från modellen",
|
||||
"imageMessages_modelReady": "Färdig",
|
||||
"imageMessages_modelNotPublished": "Ingen publikerad än — denna byggnads kan inte laddas ner av det.",
|
||||
"imageMessages_downloadFailed": "Bildmodellen kunde inte laddas ner.",
|
||||
"imageMessages_autoProcessTitle": "Automatiska bilder automatiserar",
|
||||
"imageMessages_autoProcessSubtitle": "Rekonstruera varje bild så snart den ankommer. Använder cirka 2 GB minne varje gång; låt det omför att rekonstruera genom ett tap.",
|
||||
"imageSend_title": "Send vagnbild",
|
||||
"imageSend_cropNote": "Reserverat till 512 × 512 · förhållandet mellan storlek inte bevarat",
|
||||
"imageSend_originalSize": "Ovanstående text (engelska)\n\nTranslation (svenskt):",
|
||||
"imageSend_onAirSize": "Underför hjärnan",
|
||||
"imageSend_quality": "Kvalitet",
|
||||
"imageSend_qualityStandard": "Standard",
|
||||
"imageSend_qualityHigh": "Hög",
|
||||
"imageSend_packetsLabel": "Paket",
|
||||
"imageSend_airtimeLabel": "Tid i övrighet",
|
||||
"imageSend_sizeLabel": "Ladning",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paket}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Radioindikatorer inte kända",
|
||||
"imageSend_radioUnknownBody": "Förkoppna till ett enhet så kan tiden på air beräknas.",
|
||||
"imageSend_longSendTitle": "Långförsending",
|
||||
"imageSend_longSendBody": "Detta kommer att bevara kanalen roughly {duration}.",
|
||||
"imageSend_floodNote": "Flodledning: varje repeater i ramen retransmitterar varje paket, så kan kanalen vara på arbete långare än den här tiden.",
|
||||
"imageSend_parityTitle": "Omedlningspaket",
|
||||
"imageSend_paritySubtitle": "En ytterligare paket. Gruppmeddelanden inte akceras, så den här tar hänsyn till att mottar kan återuppbygga bilden om ett enkelt paket förlorats.",
|
||||
"imageSend_send": "Sänd",
|
||||
"imageSend_cancel": "Avbud",
|
||||
"imageSend_encodeFailed": "Denna bild kan inte kodas.",
|
||||
"imageSend_codecDownloading": "Bildmodellen downloads fortfarande.",
|
||||
"imageSend_codecUnavailable": "Bildsendning är inte tillgänglig på detta enhet.",
|
||||
"imageSend_codecDisabled": "Bildmeddelanden är offna i inställningarna.",
|
||||
"imageSend_deviceUnsupported": "Denna radi kan inte senda bildpaket. Använd ett äventyrsgerät med companionsystemfirmware 13 eller nyare.",
|
||||
"imageSend_directMessagesUnsupported": "Bilder senders som gruppod, så de kan endast skickas till ett kanal – inte i en direkt meddelning.",
|
||||
"imageSend_tooLarge": "Bilden kodades till mer paket än meshformaten tillåter.",
|
||||
"imageSend_sentConfirmation": "Bilden skickades som {count} {count, plural, =1{paket} other{paket}}.",
|
||||
"imageSend_sendFailed": "Bilden kunde inte skickas: {error}",
|
||||
"imageSend_sendingProgress": "Bild sendning — paket {sent} av {total}",
|
||||
"receivedImage_senderPrefix": "Nod {prefix}",
|
||||
"receivedImage_incoming": "VANTA {received} av {total} paket",
|
||||
"receivedImage_queued": "Att väta att decodera",
|
||||
"receivedImage_tapToDecode": "Tap för att decodera",
|
||||
"receivedImage_decoding": "Rekonstruktion… om 1 sekund",
|
||||
"receivedImage_incomplete": "Bilden är okomplett — {received} av {total} paket har ankomit",
|
||||
"receivedImage_corrupt": "Bilden kunde inte rekonstrueras",
|
||||
"receivedImage_decoderMissing": "Bild mottagen — bilddekoderingen är avbrott",
|
||||
"receivedImage_evicted": "Bild inte lagrad längre",
|
||||
"receivedImage_retry": "Omförs igen",
|
||||
"receivedImage_decodeAgain": "Kodera igen",
|
||||
"receivedImage_openSettings": "Skapa en uppställning",
|
||||
"receivedImage_tapToProcess": "Tap för att behandla",
|
||||
"receivedImage_awaiting": "{bytes} byte · {packets} {packets, plural, =1{paket} other{paket}}",
|
||||
"imageSend_secondsValue": "{seconds} sekunder",
|
||||
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
|
||||
}
|
||||
|
||||
@@ -101,6 +101,8 @@
|
||||
"settings_locationInvalid": "Некоректна широта або довгота.",
|
||||
"settings_latitude": "Широта",
|
||||
"settings_longitude": "Довгота",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).",
|
||||
"settings_privacyMode": "Режим приватності",
|
||||
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
|
||||
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
|
||||
@@ -242,6 +244,19 @@
|
||||
"appSettings_last6Hours": "Останні 6 годин",
|
||||
"appSettings_last24Hours": "Останні 24 години",
|
||||
"appSettings_lastWeek": "Минулий тиждень",
|
||||
"appSettings_rasterTileSource": "Джерело растрових тайлів",
|
||||
"appSettings_stadiaEndpoint": "Кінцева точка Stadia",
|
||||
"appSettings_stadiaApiKey": "Ключ API Stadia",
|
||||
"appSettings_stadiaApiKeyRequired": "Потрібно для використання Stadia Maps",
|
||||
"appSettings_stadiaApiKeyConfigured": "Налаштовано: {maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "Введіть свій ключ API Stadia Maps. Програма використовує його для запитів растрових тайлів.",
|
||||
"appSettings_offlineMapCache": "Офлайн-кеш карти",
|
||||
"appSettings_noAreaSelected": "Область не вибрано",
|
||||
"appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})",
|
||||
@@ -779,6 +794,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "Пн {north}, Пд {south}, Сх {east}, Зх {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1117,7 +1182,7 @@
|
||||
"repeater_advancedSettings": "Просунутий",
|
||||
"repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів",
|
||||
"repeater_pathHashMode": "Режим хешування шляху",
|
||||
"repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах для виявлення потоків/петлі. 0=1 байт (256 ідентифікаторів, до 64 перехідів), 1=2 байти (65 000 ідентифікаторів, до 32 перехідів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Версії 1.13 та старіші не підтримують багатобайтні шляхи — вони активуються лише після того, як мережа буде оновлена до версії 1.14+.",
|
||||
"repeater_pathHashModeHelper": "Байти, що використовуються для кодування ідентифікатора цього ретранслятора в тегах flood-шляху/виявлення петель. 0=1 байт (256 ідентифікаторів, до 64 переходів), 1=2 байти (65 000 ідентифікаторів, до 32 переходів), 2=3 байти (16 мільйонів ідентифікаторів, до 21 переходу). Прошивки до v1.14 завжди використовували 1-байтові шляхи; v1.14 і новіші можна налаштувати на 2- або 3-байтові шляхи.",
|
||||
"repeater_txDelay": "Затримка у Flood, штат Техас",
|
||||
"repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.",
|
||||
"repeater_directTxDelay": "Пряме затримка TX",
|
||||
@@ -2520,5 +2585,227 @@
|
||||
"pathMap_noLocation": "Без геопозиції",
|
||||
"pathMap_followPacket": "Прив'язати вигляд до пакету",
|
||||
"pathMap_unfollowPacket": "Відв'язати вигляд від пакету",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS"
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "Ріси",
|
||||
"settings_regionSettingsSubtitle": "Управляти збережених регіонів",
|
||||
"settings_regionManagement_screenTitle": "Рégіональний управління",
|
||||
"settings_regionNameHint": "Назва регіону",
|
||||
"settings_regionAddRegion": "Добачить регіон",
|
||||
"settings_regionFetchRegions": "Збираємо регіони з репітерів",
|
||||
"settings_regionFetchRegionsFail": "Ні відкриті регіони",
|
||||
"settings_regionFetchRegionsAlreadyExists": "Цей регіон вже було доданий",
|
||||
"settings_regionName": "Ріонна назва",
|
||||
"settings_regionDeleted": "Ріон витертений",
|
||||
"settings_deleteRegion": "Видалення регіону",
|
||||
"settings_deleteRegionConfirm": "Вирішіть \"{region}\" з списку регіонів?",
|
||||
"settings_infoHardware": "Оборудовання",
|
||||
"settings_infoFirmware": "Програмне вміст (фirmware)",
|
||||
"repeater_pathHashModeOption0": "0 - 1 байт",
|
||||
"repeater_pathHashModeOption1": "1 - 2 байти",
|
||||
"repeater_pathHashModeOption2": "2 - 3 байти",
|
||||
"repeater_pathHashModeOption3": "3 - 4 байта",
|
||||
"appSettings_batteryLipoHv": "ЛиPo HV (3,0-4,35 В)",
|
||||
"channels_regionSetTo": "Ріон: {region}",
|
||||
"channels_regionNotSet": "Ріон: нема",
|
||||
"channels_regionSelect_Title": "Вybірте регіон",
|
||||
"channels_clearRegion": "Чистя зона",
|
||||
"chat_sendImage": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
|
||||
"chat_imagePickFailed": "Неможливо відкрити це изображення",
|
||||
"chat_receivedGif": "Підймовано GIF",
|
||||
"repeater_keySettings": "Зміна ключів ідентифікації",
|
||||
"repeater_keySettingsSubtitle": "Замініть публічний/приватний ключ",
|
||||
"repeater_prvKey": "Приватний ключ",
|
||||
"repeater_prvKeyHelper": "Нова приватна ключ для репітера, 128-символова штучна штука в шестнадцатеричному форматі.",
|
||||
"repeater_generatePrvKey": "Синхронізujте ключовий пару",
|
||||
"repeater_stopGeneratingPrvKey": "Прервити шук ключового пару",
|
||||
"repeater_pubKey": "Публічна ключ",
|
||||
"repeater_pubKeyHelper": "Це публична ключ, який пов'язано з створеною приватною ключом. Ви не можете встановлювати його напрямую.",
|
||||
"repeater_pubKeyPrefix": "Високорейтингове прикінання",
|
||||
"repeater_pubKeyPrefixHelper": "Найдіть відповідний открытий ключ, який починається ціми шестнадцатыми цифрами. Ожидається кількість потрібних попыток: {tries}.",
|
||||
"imageMessages_enableTitle": "Підтримка зображень для посилання",
|
||||
"imageMessages_enableSubtitle": "Пошукуєте изображення через сеть. Вимagaєте однократне скачання моделю изображення.",
|
||||
"imageMessages_modelSectionTitle": "Модель для зображень",
|
||||
"imageMessages_downloadModel": "Скачання",
|
||||
"imageMessages_cancelDownload": "Завершити",
|
||||
"imageMessages_removeModel": "Видалим модель",
|
||||
"imageMessages_modelReady": "Готово",
|
||||
"imageMessages_modelNotPublished": "Щоб не було опубліковано — це версія не дозволяє зробити скачання.",
|
||||
"imageMessages_downloadFailed": "Модель для зображень не можна сказати.",
|
||||
"imageMessages_autoProcessTitle": "Автоматично оброблювати obraзи",
|
||||
"imageMessages_autoProcessSubtitle": "Кожна картинка повинні бути відтворена на мій момент прибуття. Використовує приблизно 2 ГБ пам'яті на кожен раз; відключіть від відтворення за допомогою кlikу.",
|
||||
"imageSend_title": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
|
||||
"imageSend_cropNote": "Манітовано до 512 × 512 · соотношення варість не зберігається",
|
||||
"imageSend_originalSize": "Оригінал",
|
||||
"imageSend_onAirSize": "На ері",
|
||||
"imageSend_quality": "Качество",
|
||||
"imageSend_qualityStandard": "Стандарт",
|
||||
"imageSend_qualityHigh": "Висок",
|
||||
"imageSend_packetsLabel": "Пакети",
|
||||
"imageSend_airtimeLabel": "Час на ері",
|
||||
"imageSend_sizeLabel": "Підлог",
|
||||
"imageSend_packetsCount": "{count} {count, plural, one{пакет} few{пакети} many{пакетів} other{пакета}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "Настройки радио неизвестні",
|
||||
"imageSend_radioUnknownBody": "Залучіться до пристрою, щоб можна було розрахунювати час в етері.",
|
||||
"imageSend_longSendTitle": "Длуга передача",
|
||||
"imageSend_longSendBody": "Це забезпечить канал на approximativno {duration} хвилин.",
|
||||
"imageSend_floodNote": "Рухування потоків: кожен ретрансатор у діапазоні відправлює кожен пакет, тому канал залишається занятшим довше, ніж це.",
|
||||
"imageSend_parityTitle": "Пакет відновлення",
|
||||
"imageSend_paritySubtitle": "Один дополний пакет. Групові посилання не відповідають, тому це дозволяє відвідувачу відтворити зображення, якщо один пакет втрачений.",
|
||||
"imageSend_send": "Пошутить",
|
||||
"imageSend_cancel": "Завершити",
|
||||
"imageSend_encodeFailed": "Це изображення не може бути кодировано.",
|
||||
"imageSend_codecDownloading": "Модель для відтворення изображень ще збирається.",
|
||||
"imageSend_codecUnavailable": "Постій для відправлення изображень на цьому пристрої не доступний.",
|
||||
"imageSend_codecDisabled": "Пictesovani posyly wyключені в настройках.",
|
||||
"imageSend_deviceUnsupported": "Цей радіопередач не може отправляти пакети зображень. Зконнете пристрої, що працює з обладнанням керемонітом компаньєр 13 або новшим.",
|
||||
"imageSend_directMessagesUnsupported": "Фотографії передаються як дані групою, тому їх можна відправляти лише на канал — не в прямому посилання.",
|
||||
"imageSend_tooLarge": "Це изображення кодує більше пакітів, ніж дозволяє формат сети.",
|
||||
"imageSend_sentConfirmation": "Зображення надіслано як {count} {count, plural, =1{пакет} other{пакетів}}.",
|
||||
"imageSend_sendFailed": "Изображення не можна відправити: {error}",
|
||||
"imageSend_sendingProgress": "Пошускаємо изображення — пакет {sent} з {total}",
|
||||
"receivedImage_senderPrefix": "Нод {prefix}",
|
||||
"receivedImage_incoming": "Підходяче: {received} з {total} пакітів",
|
||||
"receivedImage_queued": "Очікування розслівання коду",
|
||||
"receivedImage_tapToDecode": "Кliknite для декодування",
|
||||
"receivedImage_decoding": "Повноювання… за прибуття 1 с.",
|
||||
"receivedImage_incomplete": "Образок не повний — {received} з {total} пакітів прибавлено",
|
||||
"receivedImage_corrupt": "Obraz не можна відтворити",
|
||||
"receivedImage_decoderMissing": "Під час приємки — розшифровка зображення не вдалося.",
|
||||
"receivedImage_evicted": "Изображення не зберігається",
|
||||
"receivedImage_retry": "Попробуй ще раз",
|
||||
"receivedImage_decodeAgain": "Декодуй ще раз",
|
||||
"receivedImage_openSettings": "Заведіть",
|
||||
"receivedImage_tapToProcess": "Кликніть для обробки",
|
||||
"receivedImage_awaiting": "{bytes} байтів · {packets} {packets, plural, =1{пакет} other{пакетів}}",
|
||||
"imageSend_secondsValue": "{seconds} секунди",
|
||||
"imageSend_minutesSecondsValue": "{minutes} хв {seconds} с"
|
||||
}
|
||||
|
||||
@@ -105,6 +105,8 @@
|
||||
"settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。",
|
||||
"settings_latitude": "纬度",
|
||||
"settings_longitude": "经度",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告",
|
||||
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。",
|
||||
"settings_privacyMode": "隐私模式",
|
||||
"settings_privacyModeSubtitle": "在广告中隐藏姓名/位置",
|
||||
"settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。",
|
||||
@@ -254,6 +256,19 @@
|
||||
"appSettings_last6Hours": "过去6小时",
|
||||
"appSettings_last24Hours": "过去24小时",
|
||||
"appSettings_lastWeek": "上周",
|
||||
"appSettings_rasterTileSource": "栅格瓦片源",
|
||||
"appSettings_stadiaEndpoint": "Stadia 端点",
|
||||
"appSettings_stadiaApiKey": "Stadia API 密钥",
|
||||
"appSettings_stadiaApiKeyRequired": "使用 Stadia Maps 时必需",
|
||||
"appSettings_stadiaApiKeyConfigured": "已配置:{maskedKey}",
|
||||
"@appSettings_stadiaApiKeyConfigured": {
|
||||
"placeholders": {
|
||||
"maskedKey": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"appSettings_stadiaApiKeyDialogDescription": "请输入你的 Stadia Maps API 密钥。该应用会使用它来请求栅格瓦片。",
|
||||
"appSettings_offlineMapCache": "离线地图缓存",
|
||||
"appSettings_noAreaSelected": "未选择任何区域",
|
||||
"appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom})",
|
||||
@@ -796,6 +811,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_cachedTilesLabel": "Cached tiles",
|
||||
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
|
||||
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
|
||||
"@mapCache_bulkDownloadDisabledForSource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
|
||||
"@mapCache_bulkDownloadDisabledInConfig": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summarySource": "Source: {source}",
|
||||
"@mapCache_summarySource": {
|
||||
"placeholders": {
|
||||
"source": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
|
||||
"@mapCache_summaryCachedTilesForSource": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
|
||||
"@mapCache_summaryCachedInSelection": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
|
||||
"@mapCache_summaryApproxCacheSize": {
|
||||
"placeholders": {
|
||||
"size": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"mapCache_boundsLabel": "北 {north}, 南 {south}, 东 {east}, 西 {west}",
|
||||
"@mapCache_boundsLabel": {
|
||||
"placeholders": {
|
||||
@@ -1137,7 +1202,7 @@
|
||||
"repeater_advancedSettings": "高级",
|
||||
"repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮",
|
||||
"repeater_pathHashMode": "路径哈希模式",
|
||||
"repeater_pathHashModeHelper": "用于编码此复用器的 ID 的字节数,在“洪流路径/环检测”标签中使用。 0=1 字节(256 个 ID,最多 64 个跳跃),1=2 字节(65K 个 ID,最多 32 个跳跃),2=3 字节(16M 个 ID,最多 21 个跳跃)。 v1.13 及更早版本的固件会使用多字节路径——只有在您的网络升级到 v1.14 或更高版本后才会生效。",
|
||||
"repeater_pathHashModeHelper": "用于在洪泛路径/环路检测标签中编码此中继器 ID 的字节数。0=1 字节(256 个 ID,最多 64 跳),1=2 字节(65K 个 ID,最多 32 跳),2=3 字节(16M 个 ID,最多 21 跳)。v1.14 之前的固件始终使用 1 字节路径;v1.14 及更新版本可配置为 2 或 3 字节路径。",
|
||||
"repeater_txDelay": "洪水(德克萨斯州)延误",
|
||||
"repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。",
|
||||
"repeater_directTxDelay": "直接的 TX 延迟",
|
||||
@@ -2545,5 +2610,227 @@
|
||||
"pathMap_followPacket": "锁定视图跟随数据包",
|
||||
"pathMap_unfollowPacket": "解锁视图跟随",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}"
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}",
|
||||
"@settings_deleteRegionConfirm": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@channels_regionSetTo": {
|
||||
"placeholders": {
|
||||
"region": {
|
||||
"type": "String",
|
||||
"example": "de-mitte"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@chat_imagePickFailed": {
|
||||
"description": "Shown when picking or reading a photo to send fails"
|
||||
},
|
||||
"@repeater_pubKeyPrefixHelper": {
|
||||
"placeholders": {
|
||||
"tries": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_packetsCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_range": {
|
||||
"placeholders": {
|
||||
"min": {
|
||||
"type": "String"
|
||||
},
|
||||
"max": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_longSendBody": {
|
||||
"placeholders": {
|
||||
"duration": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sentConfirmation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendFailed": {
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_sendingProgress": {
|
||||
"placeholders": {
|
||||
"sent": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_senderPrefix": {
|
||||
"placeholders": {
|
||||
"prefix": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incoming": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_incomplete": {
|
||||
"placeholders": {
|
||||
"received": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@receivedImage_awaiting": {
|
||||
"placeholders": {
|
||||
"bytes": {
|
||||
"type": "int"
|
||||
},
|
||||
"packets": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_secondsValue": {
|
||||
"placeholders": {
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@imageSend_minutesSecondsValue": {
|
||||
"placeholders": {
|
||||
"minutes": {
|
||||
"type": "String"
|
||||
},
|
||||
"seconds": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings_regionSettings": "地区",
|
||||
"settings_regionSettingsSubtitle": "管理已存储的区域",
|
||||
"settings_regionManagement_screenTitle": "区域管理",
|
||||
"settings_regionNameHint": "区域名称",
|
||||
"settings_regionAddRegion": "增加地区",
|
||||
"settings_regionFetchRegions": "从中继器获取区域",
|
||||
"settings_regionFetchRegionsFail": "未发现任何地区",
|
||||
"settings_regionFetchRegionsAlreadyExists": "该地区已被添加",
|
||||
"settings_regionName": "地区名称",
|
||||
"settings_regionDeleted": "区域已删除",
|
||||
"settings_deleteRegion": "删除区域",
|
||||
"settings_deleteRegionConfirm": "从 {region} 列表中删除?",
|
||||
"settings_infoHardware": "硬件",
|
||||
"settings_infoFirmware": "固件",
|
||||
"repeater_pathHashModeOption0": "0 - 1 字节",
|
||||
"repeater_pathHashModeOption1": "1 - 2 字节",
|
||||
"repeater_pathHashModeOption2": "2 - 3 字节",
|
||||
"repeater_pathHashModeOption3": "3 - 4 字节",
|
||||
"appSettings_batteryLipoHv": "锂多压电源(3.0-4.35伏)",
|
||||
"channels_regionSetTo": "地区:{region}",
|
||||
"channels_regionNotSet": "地区:无",
|
||||
"channels_regionSelect_Title": "选择一个地区",
|
||||
"channels_clearRegion": "无碍区域",
|
||||
"chat_sendImage": "发送图片",
|
||||
"chat_imagePickFailed": "无法打开该图片",
|
||||
"chat_receivedGif": "收到一个GIF",
|
||||
"repeater_keySettings": "更改身份密钥",
|
||||
"repeater_keySettingsSubtitle": "更换公钥/私钥对",
|
||||
"repeater_prvKey": "私钥",
|
||||
"repeater_prvKeyHelper": "中继器的新私钥,为一个128位的十六进制字符串。",
|
||||
"repeater_generatePrvKey": "生成一个随机的密钥对",
|
||||
"repeater_stopGeneratingPrvKey": "中断密钥对搜索",
|
||||
"repeater_pubKey": "公钥",
|
||||
"repeater_pubKeyHelper": "这是与生成的私钥配对的公钥。您不能直接设置此内容。",
|
||||
"repeater_pubKeyPrefix": "期望的前缀",
|
||||
"repeater_pubKeyPrefixHelper": "找到一组以这些十六进制数字开头的公钥。预计尝试次数:{tries}",
|
||||
"imageMessages_enableTitle": "启用图片消息",
|
||||
"imageMessages_enableSubtitle": "通过网格发送图像。需要一次性下载图像模型。",
|
||||
"imageMessages_modelSectionTitle": "图像模型",
|
||||
"imageMessages_downloadModel": "下载",
|
||||
"imageMessages_cancelDownload": "取消",
|
||||
"imageMessages_removeModel": "移除模型",
|
||||
"imageMessages_modelReady": "准备就绪",
|
||||
"imageMessages_modelNotPublished": "尚未发布——此版本无法下载该内容。",
|
||||
"imageMessages_downloadFailed": "图像模型无法下载。",
|
||||
"imageMessages_autoProcessTitle": "自动处理图像",
|
||||
"imageMessages_autoProcessSubtitle": "每次图像到达后立即重建,每次需要约2GB的内存;若想关闭重建功能,可通过点击取消开启。",
|
||||
"imageSend_title": "发送图片",
|
||||
"imageSend_cropNote": "调整大小至512 × 512 · 宽高比未保留",
|
||||
"imageSend_originalSize": "原文",
|
||||
"imageSend_onAirSize": "直播中",
|
||||
"imageSend_quality": "质量",
|
||||
"imageSend_qualityStandard": "标准",
|
||||
"imageSend_qualityHigh": "高",
|
||||
"imageSend_packetsLabel": "数据包",
|
||||
"imageSend_airtimeLabel": "播出时间",
|
||||
"imageSend_sizeLabel": "有效载荷",
|
||||
"imageSend_packetsCount": "{count} {count, plural, =1{个数据包} other{个数据包}}",
|
||||
"imageSend_range": "{min}–{max}",
|
||||
"imageSend_unknownValue": "—",
|
||||
"imageSend_radioUnknownTitle": "无线电设置未知",
|
||||
"imageSend_radioUnknownBody": "连接设备以计算播出时间。",
|
||||
"imageSend_longSendTitle": "长距离传输",
|
||||
"imageSend_longSendBody": "这将维持频道大约 {duration} 时间。",
|
||||
"imageSend_floodNote": "洪水路由:每个中继站都会转发每个数据包,因此信道比这段时间更长地处于繁忙状态。",
|
||||
"imageSend_parityTitle": "恢复包",
|
||||
"imageSend_paritySubtitle": "多一个数据包。群发消息不被确认,因此如果某个数据包丢失,接收方可以重新构建图像。",
|
||||
"imageSend_send": "发送",
|
||||
"imageSend_cancel": "取消",
|
||||
"imageSend_encodeFailed": "此图像无法编码。",
|
||||
"imageSend_codecDownloading": "图像模型仍在下载中。",
|
||||
"imageSend_codecUnavailable": "此设备无法发送图片。",
|
||||
"imageSend_codecDisabled": "图片消息在设置中已关闭。",
|
||||
"imageSend_deviceUnsupported": "此无线电无法发送图像数据包。请连接运行伴侣固件13或更高版本的设备。",
|
||||
"imageSend_directMessagesUnsupported": "图片作为群组数据传输,因此只能发送到频道,而不能直接发送给个人。",
|
||||
"imageSend_tooLarge": "该图像编码的数据包数量超过了网格格式所允许的范围。",
|
||||
"imageSend_sentConfirmation": "图像已作为 {count} {count, plural, =1{个数据包} other{个数据包}} 发送。",
|
||||
"imageSend_sendFailed": "无法发送图片:{error}",
|
||||
"imageSend_sendingProgress": "发送图像 — 已发送 {sent} 包含 {total}",
|
||||
"receivedImage_senderPrefix": "节点 {prefix}",
|
||||
"receivedImage_incoming": "已收到的 {received} 个包含在 {total} 个包裹中",
|
||||
"receivedImage_queued": "等待解码",
|
||||
"receivedImage_tapToDecode": "点击解锁",
|
||||
"receivedImage_decoding": "重建……大约1秒",
|
||||
"receivedImage_incomplete": "图片不完整 — {received} 已收到 {total} 个数据包",
|
||||
"receivedImage_corrupt": "图像无法重建",
|
||||
"receivedImage_decoderMissing": "收到图片 — 图像解码失败",
|
||||
"receivedImage_evicted": "图片已不再存储",
|
||||
"receivedImage_retry": "再试一次",
|
||||
"receivedImage_decodeAgain": "重新解码",
|
||||
"receivedImage_openSettings": "建立",
|
||||
"receivedImage_tapToProcess": "点击处理",
|
||||
"receivedImage_awaiting": "{bytes} 字节 · {packets} {packets, plural, =1{个数据包} other{个数据包}}",
|
||||
"imageSend_secondsValue": "{seconds} 秒",
|
||||
"imageSend_minutesSecondsValue": "{minutes} 分 {seconds} 秒"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../models/contact.dart';
|
||||
import 'app_localizations.dart';
|
||||
|
||||
@@ -23,14 +24,30 @@ extension ContactLocalization on Contact {
|
||||
}
|
||||
}
|
||||
|
||||
String pathLabel(AppLocalizations l10n) {
|
||||
String pathLabel(
|
||||
AppLocalizations l10n, {
|
||||
int pathHashByteWidth = pathHashSize,
|
||||
}) {
|
||||
if (pathOverride != null) {
|
||||
if (pathOverride! < 0) return l10n.chat_floodForced;
|
||||
if (pathOverride == 0) return l10n.chat_directForced;
|
||||
return l10n.chat_hopsForced(pathOverride!);
|
||||
return l10n.chat_hopsForced(
|
||||
_displayHopCount(pathOverrideBytes, pathOverride!, pathHashByteWidth),
|
||||
);
|
||||
}
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.chat_direct;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
return l10n.chat_hopsCount(
|
||||
_displayHopCount(path, pathLength, pathHashByteWidth),
|
||||
);
|
||||
}
|
||||
|
||||
int _displayHopCount(
|
||||
List<int>? pathBytes,
|
||||
int storedHopCount,
|
||||
int hashWidth,
|
||||
) {
|
||||
if (pathBytes == null || pathBytes.isEmpty) return storedHopCount;
|
||||
return PathHelper.splitPathBytes(pathBytes, hashWidth).length;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
@@ -9,7 +11,13 @@ import 'screens/chrome_required_screen.dart';
|
||||
import 'utils/platform_info.dart';
|
||||
|
||||
import 'connector/meshcore_connector.dart';
|
||||
import 'models/image_codec_support.dart';
|
||||
import 'screens/scanner_screen.dart';
|
||||
import 'services/image_chunk_transport.dart';
|
||||
import 'services/image_codec_service.dart';
|
||||
import 'services/image_codec_settings_store.dart';
|
||||
import 'services/received_image_blob_store_factory.dart';
|
||||
import 'services/received_image_store.dart';
|
||||
import 'services/storage_service.dart';
|
||||
import 'services/message_retry_service.dart';
|
||||
import 'services/path_history_service.dart';
|
||||
@@ -26,6 +34,7 @@ import 'services/timeout_prediction_service.dart';
|
||||
import 'storage/prefs_manager.dart';
|
||||
import 'theme/mesh_theme.dart';
|
||||
import 'utils/app_logger.dart';
|
||||
import 'widgets/image_send_codec_binding.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -51,15 +60,60 @@ void main() async {
|
||||
final bleDebugLogService = BleDebugLogService();
|
||||
final appDebugLogService = AppDebugLogService();
|
||||
final backgroundService = BackgroundService();
|
||||
final mapTileCacheService = MapTileCacheService();
|
||||
final mapTileCacheService = MapTileCacheService(
|
||||
appSettingsService: appSettingsService,
|
||||
);
|
||||
final chatTextScaleService = ChatTextScaleService();
|
||||
final translationService = TranslationService(appSettingsService);
|
||||
final uiViewStateService = UiViewStateService();
|
||||
final timeoutPredictionService = TimeoutPredictionService(storage);
|
||||
|
||||
// Load settings
|
||||
// Load settings before anything reads them. The image stack below takes its
|
||||
// model registry and its "process automatically" default straight off
|
||||
// `appSettingsService.settings`, so this cannot stay where it used to be
|
||||
// (after the constructions) without those two starting out wrong.
|
||||
await appSettingsService.loadSettings();
|
||||
|
||||
// ---- image messages (AEIC over GRP_DATA) --------------------------------
|
||||
// The codec owns the ONNX decoder; the store owns received-image state and
|
||||
// the decode queue; the reassembler/transport pair is the receive path the
|
||||
// connector feeds raw frames into.
|
||||
final imageCodecService = ImageCodecService(
|
||||
appSettingsService,
|
||||
settingsStore: AppSettingsImageCodecStore(appSettingsService),
|
||||
);
|
||||
final receivedImageStore = ReceivedImageStore(
|
||||
// Without a file-backed store this silently falls back to memory and every
|
||||
// received image — sidecar included — is gone at the next launch.
|
||||
blobs: createReceivedImageBlobStore(),
|
||||
decoder: ImageCodecServiceDecoder(imageCodecService),
|
||||
processAutomatically: appSettingsService.settings.imageProcessAutomatically,
|
||||
);
|
||||
// A decode peaks around 2.16 GiB, so the setting is the user's consent to
|
||||
// spend it. Mirrored on every settings change; the store applies it to
|
||||
// future arrivals only, so turning it on does not decode a backlog.
|
||||
appSettingsService.addListener(() {
|
||||
receivedImageStore.processAutomatically =
|
||||
appSettingsService.settings.imageProcessAutomatically;
|
||||
});
|
||||
// Availability/isBusy changes are the only thing that un-parks a decode
|
||||
// queue that stopped because the codec was unusable or busy.
|
||||
imageCodecService.addListener(receivedImageStore.notifyDecoderChanged);
|
||||
final imageReassembler = ImageStreamReassembler(store: receivedImageStore);
|
||||
final imageTransport = ImageChunkTransport(
|
||||
reassembler: imageReassembler,
|
||||
// The UI sends whole chunk sets through connector.sendImageChunks so it
|
||||
// gets real inter-chunk pacing and per-chunk progress; this closure only
|
||||
// keeps ImageChunkTransport.sendImage() usable on its own.
|
||||
send: (blob, channelIndex) => connector.sendImageChunks(
|
||||
<Uint8List>[blob],
|
||||
channelIndex: channelIndex,
|
||||
interChunkDelay: Duration.zero,
|
||||
),
|
||||
// Overwritten by onImageSenderPrefix as soon as SELF_INFO lands.
|
||||
senderPrefix: 0,
|
||||
);
|
||||
|
||||
// Initialize app logger
|
||||
appLogger.initialize(
|
||||
appDebugLogService,
|
||||
@@ -77,6 +131,8 @@ void main() async {
|
||||
|
||||
await chatTextScaleService.initialize();
|
||||
await translationService.refreshDownloadedModels();
|
||||
await imageCodecService.refreshDownloadedModels();
|
||||
await receivedImageStore.load();
|
||||
await uiViewStateService.initialize();
|
||||
await timeoutPredictionService.initialize();
|
||||
|
||||
@@ -90,6 +146,12 @@ void main() async {
|
||||
appDebugLogService: appDebugLogService,
|
||||
backgroundService: backgroundService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
imageCodecService: imageCodecService,
|
||||
imageTransport: imageTransport,
|
||||
// ImageStreamReassembler.addChunk already forwards every outcome to the
|
||||
// store together with the channel index (which ImageChunkOutcome itself
|
||||
// does not carry), so onImageChunk would be a second, channel-blind path.
|
||||
onImageSenderPrefix: (prefix) => imageReassembler.selfPrefix = prefix,
|
||||
);
|
||||
|
||||
await connector.loadContactCache();
|
||||
@@ -114,10 +176,107 @@ void main() async {
|
||||
translationService: translationService,
|
||||
uiViewStateService: uiViewStateService,
|
||||
timeoutPredictionService: timeoutPredictionService,
|
||||
imageCodecService: imageCodecService,
|
||||
receivedImageStore: receivedImageStore,
|
||||
imageReassembler: imageReassembler,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// [ReceivedImageDecoder] over [ImageCodecService].
|
||||
///
|
||||
/// Pure delegation. It exists so `received_image_store.dart` never imports
|
||||
/// `image_codec_service.dart` (and so the store stays testable without an
|
||||
/// 872 MB ONNX graph).
|
||||
class ImageCodecServiceDecoder implements ReceivedImageDecoder {
|
||||
final ImageCodecService service;
|
||||
|
||||
const ImageCodecServiceDecoder(this.service);
|
||||
|
||||
@override
|
||||
ImageCodecAvailability get availability => service.availability;
|
||||
|
||||
@override
|
||||
bool get isBusy => service.isBusy;
|
||||
|
||||
@override
|
||||
Future<ImageCodecResult?> decodeBitstream({
|
||||
required Uint8List bitstream,
|
||||
required AeicRatePoint ratePoint,
|
||||
required int resolution,
|
||||
}) => service.decodeBitstream(
|
||||
bitstream: bitstream,
|
||||
ratePoint: ratePoint,
|
||||
resolution: resolution,
|
||||
);
|
||||
|
||||
@override
|
||||
void cancelCodecJob() => service.cancelCodecJob();
|
||||
}
|
||||
|
||||
/// [ImageCodecSettingsStore] backed by [AppSettingsService].
|
||||
///
|
||||
/// Replaces `PrefsImageCodecSettingsStore` now that the five `imageCodec*`
|
||||
/// fields live on [AppSettings]; the JSON keys were already the same, so a
|
||||
/// user upgrading keeps their model registry only if it was written through
|
||||
/// this adapter — the old standalone `image_codec_settings` blob is not
|
||||
/// migrated, because a placeholder-URL build cannot have produced one.
|
||||
class AppSettingsImageCodecStore implements ImageCodecSettingsStore {
|
||||
final AppSettingsService _service;
|
||||
|
||||
const AppSettingsImageCodecStore(this._service);
|
||||
|
||||
@override
|
||||
ImageCodecPreferences get preferences => _service.settings.imageCodec;
|
||||
|
||||
@override
|
||||
Future<void> load() async {
|
||||
// AppSettingsService.loadSettings() already ran in main().
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> save(ImageCodecPreferences preferences) =>
|
||||
_service.setImageCodecPreferences(preferences);
|
||||
}
|
||||
|
||||
/// An [ImageReassembler] that (a) learns the local sender prefix after
|
||||
/// SELF_INFO and (b) forwards every outcome to [ReceivedImageStore] together
|
||||
/// with the channel index.
|
||||
///
|
||||
/// Both exist because of upstream shapes this file cannot change:
|
||||
/// * `ImageReassembler.selfPrefix` is `final`, but the local public key does
|
||||
/// not exist until `RESP_CODE_SELF_INFO`, so a reassembler built here would
|
||||
/// start with a null prefix and never be able to drop our own flood echo.
|
||||
/// Overriding the getter is the smallest fix that does not touch the
|
||||
/// transport; see the report's "needs from others".
|
||||
/// * `ImageChunkOutcome` carries no channel index, so the connector's
|
||||
/// `onImageChunk` callback cannot call `handleOutcome`, which requires one.
|
||||
/// Intercepting `addChunk` — the only place the index is in scope — can.
|
||||
class ImageStreamReassembler extends ImageReassembler {
|
||||
final ReceivedImageStore store;
|
||||
|
||||
int? _selfPrefix;
|
||||
|
||||
ImageStreamReassembler({required this.store})
|
||||
: super(onFailed: ((failure) => unawaited(store.handleFailure(failure))));
|
||||
|
||||
@override
|
||||
int? get selfPrefix => _selfPrefix;
|
||||
|
||||
set selfPrefix(int? value) => _selfPrefix = value;
|
||||
|
||||
@override
|
||||
ImageChunkOutcome addChunk(
|
||||
Uint8List blob, {
|
||||
int channelIndex = 0,
|
||||
DateTime? now,
|
||||
}) {
|
||||
final outcome = super.addChunk(blob, channelIndex: channelIndex, now: now);
|
||||
unawaited(store.handleOutcome(outcome, channelIndex: channelIndex));
|
||||
return outcome;
|
||||
}
|
||||
}
|
||||
|
||||
void _registerThirdPartyLicenses() {
|
||||
LicenseRegistry.addLicense(() async* {
|
||||
yield const LicenseEntryWithLineBreaks(
|
||||
@@ -139,7 +298,7 @@ https://creativecommons.org/licenses/by/4.0/
|
||||
});
|
||||
}
|
||||
|
||||
class MeshCoreApp extends StatelessWidget {
|
||||
class MeshCoreApp extends StatefulWidget {
|
||||
final MeshCoreConnector connector;
|
||||
final MessageRetryService retryService;
|
||||
final PathHistoryService pathHistoryService;
|
||||
@@ -152,6 +311,9 @@ class MeshCoreApp extends StatelessWidget {
|
||||
final TranslationService translationService;
|
||||
final UiViewStateService uiViewStateService;
|
||||
final TimeoutPredictionService timeoutPredictionService;
|
||||
final ImageCodecService imageCodecService;
|
||||
final ReceivedImageStore receivedImageStore;
|
||||
final ImageStreamReassembler imageReassembler;
|
||||
|
||||
const MeshCoreApp({
|
||||
super.key,
|
||||
@@ -167,24 +329,83 @@ class MeshCoreApp extends StatelessWidget {
|
||||
required this.translationService,
|
||||
required this.uiViewStateService,
|
||||
required this.timeoutPredictionService,
|
||||
required this.imageCodecService,
|
||||
required this.receivedImageStore,
|
||||
required this.imageReassembler,
|
||||
});
|
||||
|
||||
@override
|
||||
State<MeshCoreApp> createState() => _MeshCoreAppState();
|
||||
}
|
||||
|
||||
/// How often abandoned image streams are swept.
|
||||
///
|
||||
/// `ImageReassembler.evictExpired` otherwise only runs from `addChunk`, so a
|
||||
/// sender that stops mid-image would leave its bubble stuck on "2 of 3
|
||||
/// packets" until some unrelated image arrived.
|
||||
const Duration _kImageSweepInterval = Duration(seconds: 5);
|
||||
|
||||
class _MeshCoreAppState extends State<MeshCoreApp> with WidgetsBindingObserver {
|
||||
Timer? _imageSweepTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_imageSweepTimer = Timer.periodic(
|
||||
_kImageSweepInterval,
|
||||
(_) => widget.imageReassembler.evictExpired(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_imageSweepTimer?.cancel();
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
super.didChangeAppLifecycleState(state);
|
||||
widget.receivedImageStore.setForeground(state == AppLifecycleState.resumed);
|
||||
// ~2.7 GiB resident in a backgrounded app is a low-memory-killer kill, so
|
||||
// unlike the translation stack the graph is dropped on background rather
|
||||
// than held until the radio disconnects.
|
||||
if (state == AppLifecycleState.paused ||
|
||||
state == AppLifecycleState.hidden) {
|
||||
unawaited(widget.imageCodecService.handleMemoryPressure());
|
||||
unawaited(widget.receivedImageStore.handleMemoryPressure());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void didHaveMemoryPressure() {
|
||||
super.didHaveMemoryPressure();
|
||||
unawaited(widget.imageCodecService.handleMemoryPressure());
|
||||
unawaited(widget.receivedImageStore.handleMemoryPressure());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final connector = widget.connector;
|
||||
final storage = widget.storage;
|
||||
return MultiProvider(
|
||||
providers: [
|
||||
ChangeNotifierProvider.value(value: connector),
|
||||
ChangeNotifierProvider.value(value: retryService),
|
||||
ChangeNotifierProvider.value(value: pathHistoryService),
|
||||
ChangeNotifierProvider.value(value: appSettingsService),
|
||||
ChangeNotifierProvider.value(value: bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: translationService),
|
||||
ChangeNotifierProvider.value(value: uiViewStateService),
|
||||
ChangeNotifierProvider.value(value: widget.retryService),
|
||||
ChangeNotifierProvider.value(value: widget.pathHistoryService),
|
||||
ChangeNotifierProvider.value(value: widget.appSettingsService),
|
||||
ChangeNotifierProvider.value(value: widget.bleDebugLogService),
|
||||
ChangeNotifierProvider.value(value: widget.appDebugLogService),
|
||||
ChangeNotifierProvider.value(value: widget.chatTextScaleService),
|
||||
ChangeNotifierProvider.value(value: widget.translationService),
|
||||
ChangeNotifierProvider.value(value: widget.uiViewStateService),
|
||||
Provider.value(value: storage),
|
||||
Provider.value(value: mapTileCacheService),
|
||||
ChangeNotifierProvider.value(value: timeoutPredictionService),
|
||||
ChangeNotifierProvider.value(value: widget.mapTileCacheService),
|
||||
ChangeNotifierProvider.value(value: widget.timeoutPredictionService),
|
||||
ChangeNotifierProvider.value(value: widget.imageCodecService),
|
||||
ChangeNotifierProvider.value(value: widget.receivedImageStore),
|
||||
],
|
||||
child: Consumer<AppSettingsService>(
|
||||
builder: (context, settingsService, child) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'image_codec_support.dart';
|
||||
import 'translation_support.dart';
|
||||
|
||||
enum UnitSystem { metric, imperial }
|
||||
@@ -76,6 +77,7 @@ class Cyr2LatProfile {
|
||||
|
||||
class AppSettings {
|
||||
static const Object _unset = Object();
|
||||
static const String stadiaDemo = '51bd0381-4685-4666-bae8-48940f6d77c0';
|
||||
|
||||
final bool clearPathOnMaxRetry;
|
||||
final bool mapShowRepeaters;
|
||||
@@ -91,10 +93,15 @@ class AppSettings {
|
||||
final Map<String, double>? mapCacheBounds;
|
||||
final int mapCacheMinZoom;
|
||||
final int mapCacheMaxZoom;
|
||||
final String mapRasterSourceId;
|
||||
final String mapTileEndpointId;
|
||||
final String? mapTileApiKey;
|
||||
final bool notificationsEnabled;
|
||||
final bool notifyOnNewMessage;
|
||||
final bool notifyOnNewChannelMessage;
|
||||
final bool notifyOnNewAdvert;
|
||||
final bool autoSendZeroHopAdvertOnGpsUpdate;
|
||||
final int gpsIntervalSeconds;
|
||||
final bool autoRouteRotationEnabled;
|
||||
final double maxRouteWeight;
|
||||
final double initialRouteWeight;
|
||||
@@ -112,6 +119,29 @@ class AppSettings {
|
||||
final String tcpServerAddress;
|
||||
final int tcpServerPort;
|
||||
final bool jumpToOldestUnread;
|
||||
final bool imageMessagesEnabled;
|
||||
|
||||
/// Whether a received image is decoded as soon as it is reassembled.
|
||||
///
|
||||
/// Off by default and deliberately so: a decode peaks around 2.16 GiB
|
||||
/// resident and takes about a second, so an unattended chat must not be able
|
||||
/// to trigger one per arriving image. When false, `ReceivedImageStore` parks
|
||||
/// the arrival as a "Tap to process" placeholder instead of queueing it.
|
||||
final bool imageProcessAutomatically;
|
||||
|
||||
// ---- neural image codec (AEIC-SE) ---------------------------------------
|
||||
// Structural twins of the translation block above; the JSON keys match
|
||||
// ImageCodecPreferences.toJson so ImageCodecService reads them unchanged.
|
||||
final bool imageCodecEnabled;
|
||||
final String? imageCodecSelectedModelId;
|
||||
final String? imageCodecModelSourceUrl;
|
||||
|
||||
/// [AeicRatePoint.wireValue] of the composer's default rate point.
|
||||
/// 4 == ft32, the only rate point this build ships.
|
||||
final int imageCodecRatePoint;
|
||||
|
||||
final List<ImageCodecModelRecord> imageCodecDownloadedModels;
|
||||
|
||||
final bool translationEnabled;
|
||||
final bool autoTranslateIncomingMessages;
|
||||
final String? translationTargetLanguageCode;
|
||||
@@ -122,6 +152,26 @@ class AppSettings {
|
||||
final List<Cyr2LatProfile> cyr2latProfiles;
|
||||
final String selectedCyr2latProfileId;
|
||||
|
||||
/// The five `imageCodec*` fields as the value object `ImageCodecService`
|
||||
/// consumes. Assembled rather than stored so the settings blob stays flat.
|
||||
ImageCodecPreferences get imageCodec => ImageCodecPreferences(
|
||||
enabled: imageCodecEnabled,
|
||||
selectedModelId: imageCodecSelectedModelId,
|
||||
modelSourceUrl: imageCodecModelSourceUrl,
|
||||
ratePoint: imageCodecRatePoint,
|
||||
downloadedModels: imageCodecDownloadedModels,
|
||||
);
|
||||
|
||||
String get effectiveMapTileApiKey {
|
||||
final apiKey = mapTileApiKey?.trim();
|
||||
if (apiKey == null || apiKey.isEmpty) {
|
||||
return stadiaDemo;
|
||||
}
|
||||
return apiKey;
|
||||
}
|
||||
|
||||
bool get usesstadiaDemo => effectiveMapTileApiKey == stadiaDemo;
|
||||
|
||||
Map<String, String> get cyr2latCharMap {
|
||||
final profile = cyr2latProfiles.firstWhere(
|
||||
(p) => p.id == selectedCyr2latProfileId,
|
||||
@@ -145,10 +195,15 @@ class AppSettings {
|
||||
this.mapCacheBounds,
|
||||
this.mapCacheMinZoom = 10,
|
||||
this.mapCacheMaxZoom = 15,
|
||||
this.mapRasterSourceId = 'osm_auto',
|
||||
this.mapTileEndpointId = 'standard_2x',
|
||||
this.mapTileApiKey,
|
||||
this.notificationsEnabled = true,
|
||||
this.notifyOnNewMessage = true,
|
||||
this.notifyOnNewChannelMessage = true,
|
||||
this.notifyOnNewAdvert = true,
|
||||
this.autoSendZeroHopAdvertOnGpsUpdate = false,
|
||||
this.gpsIntervalSeconds = 900,
|
||||
this.autoRouteRotationEnabled = true,
|
||||
this.maxRouteWeight = 5.0,
|
||||
this.initialRouteWeight = 3.0,
|
||||
@@ -166,6 +221,13 @@ class AppSettings {
|
||||
this.tcpServerAddress = '',
|
||||
this.tcpServerPort = 0,
|
||||
this.jumpToOldestUnread = false,
|
||||
this.imageMessagesEnabled = false,
|
||||
this.imageProcessAutomatically = false,
|
||||
this.imageCodecEnabled = false,
|
||||
this.imageCodecSelectedModelId,
|
||||
this.imageCodecModelSourceUrl,
|
||||
this.imageCodecRatePoint = 4,
|
||||
List<ImageCodecModelRecord>? imageCodecDownloadedModels,
|
||||
this.translationEnabled = false,
|
||||
this.autoTranslateIncomingMessages = true,
|
||||
this.translationTargetLanguageCode,
|
||||
@@ -178,6 +240,7 @@ class AppSettings {
|
||||
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
|
||||
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
|
||||
mutedChannels = mutedChannels ?? {},
|
||||
imageCodecDownloadedModels = imageCodecDownloadedModels ?? const [],
|
||||
translationDownloadedModels = translationDownloadedModels ?? const [],
|
||||
cyr2latProfiles =
|
||||
cyr2latProfiles ??
|
||||
@@ -206,10 +269,16 @@ class AppSettings {
|
||||
'map_cache_bounds': mapCacheBounds,
|
||||
'map_cache_min_zoom': mapCacheMinZoom,
|
||||
'map_cache_max_zoom': mapCacheMaxZoom,
|
||||
'map_raster_source_id': mapRasterSourceId,
|
||||
'map_tile_endpoint_id': mapTileEndpointId,
|
||||
'map_tile_api_key': mapTileApiKey,
|
||||
'notifications_enabled': notificationsEnabled,
|
||||
'notify_on_new_message': notifyOnNewMessage,
|
||||
'notify_on_new_channel_message': notifyOnNewChannelMessage,
|
||||
'notify_on_new_advert': notifyOnNewAdvert,
|
||||
'auto_send_zero_hop_advert_on_gps_update':
|
||||
autoSendZeroHopAdvertOnGpsUpdate,
|
||||
'gps_interval_seconds': gpsIntervalSeconds,
|
||||
'auto_route_rotation_enabled': autoRouteRotationEnabled,
|
||||
'max_route_weight': maxRouteWeight,
|
||||
'initial_route_weight': initialRouteWeight,
|
||||
@@ -227,6 +296,15 @@ class AppSettings {
|
||||
'tcp_server_address': tcpServerAddress,
|
||||
'tcp_server_port': tcpServerPort,
|
||||
'jump_to_oldest_unread': jumpToOldestUnread,
|
||||
'image_messages_enabled': imageMessagesEnabled,
|
||||
'image_process_automatically': imageProcessAutomatically,
|
||||
'image_codec_enabled': imageCodecEnabled,
|
||||
'image_codec_selected_model_id': imageCodecSelectedModelId,
|
||||
'image_codec_model_source_url': imageCodecModelSourceUrl,
|
||||
'image_codec_rate_point': imageCodecRatePoint,
|
||||
'image_codec_downloaded_models': imageCodecDownloadedModels
|
||||
.map((model) => model.toJson())
|
||||
.toList(),
|
||||
'translation_enabled': translationEnabled,
|
||||
'auto_translate_incoming_messages': autoTranslateIncomingMessages,
|
||||
'translation_target_language_code': translationTargetLanguageCode,
|
||||
@@ -270,11 +348,18 @@ class AppSettings {
|
||||
),
|
||||
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
|
||||
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15,
|
||||
mapRasterSourceId: json['map_raster_source_id'] as String? ?? 'osm_auto',
|
||||
mapTileEndpointId: json['map_tile_endpoint_id'] as String? ?? 'standard',
|
||||
mapTileApiKey: json['map_tile_api_key'] as String?,
|
||||
notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
|
||||
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
|
||||
notifyOnNewChannelMessage:
|
||||
json['notify_on_new_channel_message'] as bool? ?? true,
|
||||
notifyOnNewAdvert: json['notify_on_new_advert'] as bool? ?? true,
|
||||
autoSendZeroHopAdvertOnGpsUpdate:
|
||||
json['auto_send_zero_hop_advert_on_gps_update'] as bool? ?? false,
|
||||
gpsIntervalSeconds:
|
||||
(json['gps_interval_seconds'] as num?)?.toInt() ?? 900,
|
||||
autoRouteRotationEnabled:
|
||||
json['auto_route_rotation_enabled'] as bool? ?? true,
|
||||
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
|
||||
@@ -309,6 +394,23 @@ class AppSettings {
|
||||
tcpServerAddress: json['tcp_server_address'] as String? ?? '',
|
||||
tcpServerPort: json['tcp_server_port'] as int? ?? 0,
|
||||
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false,
|
||||
imageMessagesEnabled: json['image_messages_enabled'] as bool? ?? false,
|
||||
imageProcessAutomatically:
|
||||
json['image_process_automatically'] as bool? ?? false,
|
||||
imageCodecEnabled: json['image_codec_enabled'] as bool? ?? false,
|
||||
imageCodecSelectedModelId:
|
||||
json['image_codec_selected_model_id'] as String?,
|
||||
imageCodecModelSourceUrl: json['image_codec_model_source_url'] as String?,
|
||||
imageCodecRatePoint: json['image_codec_rate_point'] as int? ?? 4,
|
||||
imageCodecDownloadedModels:
|
||||
(json['image_codec_downloaded_models'] as List<dynamic>?)
|
||||
?.map(
|
||||
(entry) => ImageCodecModelRecord.fromJson(
|
||||
Map<String, dynamic>.from(entry as Map),
|
||||
),
|
||||
)
|
||||
.toList() ??
|
||||
const [],
|
||||
translationEnabled: json['translation_enabled'] as bool? ?? false,
|
||||
autoTranslateIncomingMessages:
|
||||
json['auto_translate_incoming_messages'] as bool? ?? true,
|
||||
@@ -379,10 +481,15 @@ class AppSettings {
|
||||
Object? mapCacheBounds = _unset,
|
||||
int? mapCacheMinZoom,
|
||||
int? mapCacheMaxZoom,
|
||||
String? mapRasterSourceId,
|
||||
String? mapTileEndpointId,
|
||||
Object? mapTileApiKey = _unset,
|
||||
bool? notificationsEnabled,
|
||||
bool? notifyOnNewMessage,
|
||||
bool? notifyOnNewChannelMessage,
|
||||
bool? notifyOnNewAdvert,
|
||||
bool? autoSendZeroHopAdvertOnGpsUpdate,
|
||||
int? gpsIntervalSeconds,
|
||||
bool? autoRouteRotationEnabled,
|
||||
double? maxRouteWeight,
|
||||
double? initialRouteWeight,
|
||||
@@ -400,6 +507,13 @@ class AppSettings {
|
||||
String? tcpServerAddress,
|
||||
int? tcpServerPort,
|
||||
bool? jumpToOldestUnread,
|
||||
bool? imageMessagesEnabled,
|
||||
bool? imageProcessAutomatically,
|
||||
bool? imageCodecEnabled,
|
||||
Object? imageCodecSelectedModelId = _unset,
|
||||
Object? imageCodecModelSourceUrl = _unset,
|
||||
int? imageCodecRatePoint,
|
||||
List<ImageCodecModelRecord>? imageCodecDownloadedModels,
|
||||
bool? translationEnabled,
|
||||
bool? autoTranslateIncomingMessages,
|
||||
Object? translationTargetLanguageCode = _unset,
|
||||
@@ -428,11 +542,20 @@ class AppSettings {
|
||||
: mapCacheBounds as Map<String, double>?,
|
||||
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
|
||||
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
|
||||
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
|
||||
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
|
||||
mapTileApiKey: mapTileApiKey == _unset
|
||||
? this.mapTileApiKey
|
||||
: mapTileApiKey as String?,
|
||||
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
|
||||
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
|
||||
notifyOnNewChannelMessage:
|
||||
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
|
||||
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
|
||||
autoSendZeroHopAdvertOnGpsUpdate:
|
||||
autoSendZeroHopAdvertOnGpsUpdate ??
|
||||
this.autoSendZeroHopAdvertOnGpsUpdate,
|
||||
gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds,
|
||||
autoRouteRotationEnabled:
|
||||
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
|
||||
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
|
||||
@@ -458,6 +581,19 @@ class AppSettings {
|
||||
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
|
||||
tcpServerPort: tcpServerPort ?? this.tcpServerPort,
|
||||
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread,
|
||||
imageMessagesEnabled: imageMessagesEnabled ?? this.imageMessagesEnabled,
|
||||
imageProcessAutomatically:
|
||||
imageProcessAutomatically ?? this.imageProcessAutomatically,
|
||||
imageCodecEnabled: imageCodecEnabled ?? this.imageCodecEnabled,
|
||||
imageCodecSelectedModelId: imageCodecSelectedModelId == _unset
|
||||
? this.imageCodecSelectedModelId
|
||||
: imageCodecSelectedModelId as String?,
|
||||
imageCodecModelSourceUrl: imageCodecModelSourceUrl == _unset
|
||||
? this.imageCodecModelSourceUrl
|
||||
: imageCodecModelSourceUrl as String?,
|
||||
imageCodecRatePoint: imageCodecRatePoint ?? this.imageCodecRatePoint,
|
||||
imageCodecDownloadedModels:
|
||||
imageCodecDownloadedModels ?? this.imageCodecDownloadedModels,
|
||||
translationEnabled: translationEnabled ?? this.translationEnabled,
|
||||
autoTranslateIncomingMessages:
|
||||
autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages,
|
||||
|
||||
@@ -27,6 +27,10 @@ class Channel {
|
||||
|
||||
bool get isPublicChannel => pskHex == publicChannelPsk;
|
||||
|
||||
bool get isHashtagChannel => name.startsWith('#');
|
||||
|
||||
bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel;
|
||||
|
||||
static Channel? fromFrame(Uint8List frame) {
|
||||
// CHANNEL_INFO format:
|
||||
// [0] = RESP_CODE_CHANNEL_INFO (18)
|
||||
|
||||
@@ -41,6 +41,7 @@ class ChannelMessage {
|
||||
final List<Repeat> repeats;
|
||||
final int repeatCount;
|
||||
final int? pathLength;
|
||||
final int? pathHashWidth;
|
||||
final Uint8List pathBytes;
|
||||
final List<Uint8List> pathVariants;
|
||||
final int? channelIndex;
|
||||
@@ -66,6 +67,7 @@ class ChannelMessage {
|
||||
this.repeats = const [],
|
||||
this.repeatCount = 0,
|
||||
this.pathLength,
|
||||
this.pathHashWidth,
|
||||
Uint8List? pathBytes,
|
||||
List<Uint8List>? pathVariants,
|
||||
this.channelIndex,
|
||||
@@ -93,6 +95,7 @@ class ChannelMessage {
|
||||
List<Repeat>? repeats,
|
||||
int? repeatCount,
|
||||
int? pathLength,
|
||||
int? pathHashWidth,
|
||||
Uint8List? pathBytes,
|
||||
List<Uint8List>? pathVariants,
|
||||
String? packetHash,
|
||||
@@ -129,6 +132,7 @@ class ChannelMessage {
|
||||
repeats: repeats ?? this.repeats,
|
||||
repeatCount: repeatCount ?? this.repeatCount,
|
||||
pathLength: pathLength ?? this.pathLength,
|
||||
pathHashWidth: pathHashWidth ?? this.pathHashWidth,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
pathVariants: pathVariants ?? this.pathVariants,
|
||||
channelIndex: channelIndex,
|
||||
@@ -155,6 +159,7 @@ class ChannelMessage {
|
||||
|
||||
int pathLen;
|
||||
int txtType;
|
||||
int? packetPathHashWidth;
|
||||
Uint8List pathBytes = Uint8List(0);
|
||||
int channelIdx;
|
||||
if (code == respCodeChannelMsgRecvV3) {
|
||||
@@ -163,12 +168,18 @@ class ChannelMessage {
|
||||
final hasPath = (flags & 0x01) != 0;
|
||||
reader.skipBytes(1); // Skip reserved byte
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
txtType = reader.readByte();
|
||||
if (hasPath && pathLen > 0) {
|
||||
reader.rewind(); // Rewind to read path length again for pathBytes
|
||||
pathBytes = reader.readBytes(pathLen);
|
||||
final pathByte = reader.readUInt8();
|
||||
// pathByte packs: top 2 bits = hash width mode, low 6 bits = hop count
|
||||
packetPathHashWidth = ((pathByte & 0xC0) >> 6) + 1;
|
||||
final hopCount = pathByte & 0x3F;
|
||||
pathLen = hopCount;
|
||||
// If a path is present, read hopCount * width bytes
|
||||
if (hasPath && hopCount > 0) {
|
||||
final totalPathBytes = hopCount * packetPathHashWidth;
|
||||
pathBytes = reader.readBytes(totalPathBytes);
|
||||
}
|
||||
// After consuming optional path bytes, read the text type byte.
|
||||
txtType = reader.readByte();
|
||||
} else {
|
||||
channelIdx = reader.readByte();
|
||||
pathLen = reader.readInt8();
|
||||
@@ -209,6 +220,7 @@ class ChannelMessage {
|
||||
isOutgoing: false,
|
||||
status: ChannelMessageStatus.sent,
|
||||
pathLength: pathLen,
|
||||
pathHashWidth: packetPathHashWidth,
|
||||
pathBytes: pathBytes,
|
||||
channelIndex: channelIdx,
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:crypto/crypto.dart' as crypto;
|
||||
import 'package:meshcore_open/utils/keys.dart';
|
||||
|
||||
import 'channel.dart';
|
||||
|
||||
@@ -37,11 +37,7 @@ class Community {
|
||||
|
||||
/// Generate a new community with a random 32-byte secret
|
||||
factory Community.create({required String id, required String name}) {
|
||||
final random = Random.secure();
|
||||
final secret = Uint8List(32);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
secret[i] = random.nextInt(256);
|
||||
}
|
||||
final secret = randomBytes(32);
|
||||
return Community(
|
||||
id: id,
|
||||
name: name,
|
||||
@@ -210,12 +206,7 @@ class Community {
|
||||
|
||||
/// Create a copy of this community with a regenerated random secret
|
||||
Community withRegeneratedSecret() {
|
||||
final random = Random.secure();
|
||||
final newSecret = Uint8List(32);
|
||||
for (int i = 0; i < 32; i++) {
|
||||
newSecret[i] = random.nextInt(256);
|
||||
}
|
||||
return withNewSecret(newSecret);
|
||||
return withNewSecret(randomBytes(32));
|
||||
}
|
||||
|
||||
/// Extract secret from QR data (for updating existing community)
|
||||
|
||||
@@ -119,7 +119,7 @@ class Contact {
|
||||
String pathFormattedIdList(int hashByteWidth) {
|
||||
final pathBytes = pathBytesForDisplay;
|
||||
if (pathBytes.isEmpty) return '';
|
||||
final w = hashByteWidth.clamp(1, 8);
|
||||
final w = hashByteWidth.clamp(1, 4);
|
||||
final parts = <String>[];
|
||||
for (int i = 0; i < pathBytes.length; i += w) {
|
||||
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
|
||||
@@ -166,8 +166,18 @@ class Contact {
|
||||
final type = reader.readByte();
|
||||
final flags = reader.readByte();
|
||||
final pathLen = reader.readByte();
|
||||
final safePathLen = pathLen > 0
|
||||
? (pathLen > maxPathSize ? maxPathSize : pathLen)
|
||||
int hopCount = 0;
|
||||
int byteLen = 0;
|
||||
if (pathLen == 0xFF) {
|
||||
hopCount = -1;
|
||||
} else {
|
||||
final mode = (pathLen & 0xC0) >> 6;
|
||||
hopCount = pathLen & 0x3F;
|
||||
final width = mode + 1;
|
||||
byteLen = hopCount * width;
|
||||
}
|
||||
final safePathLen = byteLen > 0
|
||||
? (byteLen > maxPathSize ? maxPathSize : byteLen)
|
||||
: 0;
|
||||
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
|
||||
final name = reader.readCStringGreedy(maxNameSize);
|
||||
@@ -213,7 +223,7 @@ class Contact {
|
||||
name: name.isEmpty ? 'Unknown' : name,
|
||||
type: type,
|
||||
flags: flags,
|
||||
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen,
|
||||
pathLength: hopCount,
|
||||
path: pathBytes,
|
||||
latitude: lat,
|
||||
longitude: lon,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:latlong2/latlong.dart';
|
||||
@@ -13,8 +14,8 @@ class DisplayPath {
|
||||
final Color color;
|
||||
final bool isPrimary;
|
||||
|
||||
/// Outbound hop bytes, including hops that could not be placed on the map.
|
||||
final List<int> hopBytes;
|
||||
/// Outbound hop prefixes, including hops that could not be placed on the map.
|
||||
final List<Uint8List> hopBytes;
|
||||
|
||||
/// Resolved map points: self, each locatable hop, then the target when its
|
||||
/// position is known. Hops with no position are skipped here but still
|
||||
|
||||
@@ -0,0 +1,835 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../widgets/image_send_codec_binding.dart';
|
||||
|
||||
/// Model registry + value types for the neural image codec (AEIC-SE).
|
||||
///
|
||||
/// Mirrors `lib/models/translation_support.dart` field-for-field so the
|
||||
/// settings screen, the file store and the download machinery can be copied
|
||||
/// from the translation stack with no structural changes.
|
||||
|
||||
enum ImageCodecStatus { none, pending, completed, failed, skipped }
|
||||
|
||||
extension ImageCodecStatusValue on ImageCodecStatus {
|
||||
String get value {
|
||||
switch (this) {
|
||||
case ImageCodecStatus.pending:
|
||||
return 'pending';
|
||||
case ImageCodecStatus.completed:
|
||||
return 'completed';
|
||||
case ImageCodecStatus.failed:
|
||||
return 'failed';
|
||||
case ImageCodecStatus.skipped:
|
||||
return 'skipped';
|
||||
case ImageCodecStatus.none:
|
||||
return 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImageCodecStatus parseImageCodecStatus(dynamic value) {
|
||||
if (value is! String) {
|
||||
return ImageCodecStatus.none;
|
||||
}
|
||||
for (final status in ImageCodecStatus.values) {
|
||||
if (status.value == value) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
return ImageCodecStatus.none;
|
||||
}
|
||||
|
||||
/// Shape of the decoder's only input tensor: `y_hat`, float32,
|
||||
/// `[1, 256, 16, 16]`. Static — the export has no dynamic axes and no batching.
|
||||
const List<int> kImageCodecLatentShape = <int>[1, 256, 16, 16];
|
||||
|
||||
/// Element count implied by [kImageCodecLatentShape] (65,536).
|
||||
const int kImageCodecLatentElements = 256 * 16 * 16;
|
||||
|
||||
/// Graph input/output names in the promoted export. Used as a preference when
|
||||
/// binding tensors; the backend falls back to the graph's sole input/output if
|
||||
/// a future re-export renames them.
|
||||
const String kImageCodecDecoderInputName = 'y_hat';
|
||||
const String kImageCodecDecoderOutputName = 'image';
|
||||
|
||||
/// Rate point = which `AEIC_SE_ft*.pkl` the encoder was fine-tuned from.
|
||||
///
|
||||
/// **ft32 IS THE ONLY SHIPPING RATE POINT** ([kShippingAeicRatePoint]). One
|
||||
/// checkpoint ships, so [aeicRatePointForUi] is constant and the UI has no
|
||||
/// quality selector to honour. The other members are retained *only* because
|
||||
/// their ordinals are already persisted in `image_codec_rate_point` and cross
|
||||
/// the isolate boundary, and because the measured byte counts below are real
|
||||
/// data worth keeping. Do not reorder, do not add a second shipping entry
|
||||
/// without a second model in [imageCodecPresetModels].
|
||||
///
|
||||
/// NOTE ON THE WIRE VALUE. This enum's ordinal is NOT the on-air rate nibble.
|
||||
/// The on-air chunk-0 metadata byte is defined by `ImageStreamMetadata` in
|
||||
/// `lib/services/image_chunk_transport.dart` and carries
|
||||
/// [ImageCodecRatePoint.index], because the transport only ever exposes UI rate
|
||||
/// points. [AeicRatePointValue]'s `wireValue` is the *model-selection ordinal*:
|
||||
/// it is what gets persisted in settings and what crosses the isolate boundary
|
||||
/// to pick a checkpoint. Convert with [aeicRatePointForUi] /
|
||||
/// [uiRatePointForAeic]; never assume they are the same integer.
|
||||
///
|
||||
/// Measured real rANS bitstream sizes at 512x512 over 26 images
|
||||
/// (Kodak 24 + 2 custom). `ft2`/`ft4` have not been measured on this corpus.
|
||||
enum AeicRatePoint { ft2, ft4, ft8, ft16, ft32 }
|
||||
|
||||
/// The one rate point this build encodes and decodes at.
|
||||
///
|
||||
/// ft32: mean 155.8 B, min 110 B, max 209 B over the 26-image corpus, i.e. 1-2
|
||||
/// data chunks plus one XOR parity chunk. ft16 was dropped: it needs 2-3 data
|
||||
/// chunks and leaves only 79 B of headroom under the 3-chunk ceiling.
|
||||
const AeicRatePoint kShippingAeicRatePoint = AeicRatePoint.ft32;
|
||||
|
||||
extension AeicRatePointValue on AeicRatePoint {
|
||||
int get wireValue => index;
|
||||
|
||||
/// Mean measured bitstream size in bytes, or `null` where unmeasured.
|
||||
int? get meanBytes {
|
||||
switch (this) {
|
||||
case AeicRatePoint.ft2:
|
||||
case AeicRatePoint.ft4:
|
||||
return null;
|
||||
case AeicRatePoint.ft8:
|
||||
return 507;
|
||||
case AeicRatePoint.ft16:
|
||||
return 288;
|
||||
case AeicRatePoint.ft32:
|
||||
return 156;
|
||||
}
|
||||
}
|
||||
|
||||
/// Largest bitstream observed on the measurement corpus, or `null`.
|
||||
int? get maxBytes {
|
||||
switch (this) {
|
||||
case AeicRatePoint.ft2:
|
||||
case AeicRatePoint.ft4:
|
||||
return null;
|
||||
case AeicRatePoint.ft8:
|
||||
return 732;
|
||||
case AeicRatePoint.ft16:
|
||||
return 409;
|
||||
case AeicRatePoint.ft32:
|
||||
return 209;
|
||||
}
|
||||
}
|
||||
|
||||
/// `'ft32 (~156 B)'`. Uses the enum's own `name` for the prefix — do not add
|
||||
/// a `name` getter here, it would collide with `dart:core`'s `EnumName`.
|
||||
String get label {
|
||||
final mean = meanBytes;
|
||||
return mean == null ? name : '$name (~$mean B)';
|
||||
}
|
||||
}
|
||||
|
||||
AeicRatePoint parseAeicRatePoint(int wireValue) {
|
||||
if (wireValue < 0 || wireValue >= AeicRatePoint.values.length) {
|
||||
return kShippingAeicRatePoint;
|
||||
}
|
||||
return AeicRatePoint.values[wireValue];
|
||||
}
|
||||
|
||||
/// Maps the UI-level rate selector onto the codec's rate points.
|
||||
///
|
||||
/// ft32-only, so this is deliberately constant and ignores [rate]. It is kept
|
||||
/// as a function (rather than inlining [kShippingAeicRatePoint] at the call
|
||||
/// sites) so that adding a second checkpoint is a one-function change, and so
|
||||
/// this file never names an [ImageCodecRatePoint] member — that enum is owned by
|
||||
/// `image_send_codec_binding.dart` and its membership is the UI's to shrink.
|
||||
AeicRatePoint aeicRatePointForUi(ImageCodecRatePoint rate) =>
|
||||
kShippingAeicRatePoint;
|
||||
|
||||
/// Inverse of [aeicRatePointForUi]. Constant for the same reason: every rate
|
||||
/// point this build can produce is the standard one.
|
||||
ImageCodecRatePoint uiRatePointForAeic(AeicRatePoint rate) =>
|
||||
ImageCodecRatePoint.standard;
|
||||
|
||||
/// Bundle layout revision written into [ImageCodecModelRecord.bundleVersion].
|
||||
///
|
||||
/// 0 means "installed before the entropy bundle existed", i.e. a decoder-only
|
||||
/// install that can render a latent but cannot encode or decode a bitstream.
|
||||
/// Bump this only when the *set of files* a model needs changes; it is what
|
||||
/// `ImageCodecService.needsModelUpgrade` keys off.
|
||||
///
|
||||
/// 1 -> 2: the bundle gained the **decode-side** entropy graph
|
||||
/// (`aeic_entropy_decode_fp32_op17.onnx`). A version-1 install has the
|
||||
/// send-side graph only, so it can encode but cannot decode a bitstream; the
|
||||
/// remedy is a re-download, which is exactly what `needsModelUpgrade` asks for.
|
||||
const int kImageCodecBundleVersion = 2;
|
||||
|
||||
/// Same shape and field order as `TranslationModelRecord`, plus the two fields
|
||||
/// a multi-asset install needs.
|
||||
class ImageCodecModelRecord {
|
||||
final String id;
|
||||
final String name;
|
||||
final String sourceUrl;
|
||||
|
||||
/// Path of the **decoder graph** — the file ONNX Runtime is handed. The other
|
||||
/// assets are its siblings in the same directory; see [assetFileNames].
|
||||
final String localPath;
|
||||
|
||||
final DateTime downloadedAt;
|
||||
final int fileSizeBytes;
|
||||
|
||||
/// Every file that was installed for this model, by exact on-disk name.
|
||||
///
|
||||
/// Recorded rather than re-derived so the service can locate the entropy
|
||||
/// graph and the CDF tables without guessing filenames, and so `deleteModel`
|
||||
/// can remove all ~1.0 GB instead of just the 3 MB graph.
|
||||
final List<String> assetFileNames;
|
||||
|
||||
/// What each installed file IS, keyed by its exact on-disk name.
|
||||
///
|
||||
/// Recorded at install time because [assetFileNames] alone stopped being
|
||||
/// enough at bundle version 2: the bundle now holds THREE `.onnx` files —
|
||||
/// the synthesis decoder, the send-side entropy graph and the decode-side
|
||||
/// entropy graph — and no property of a filename distinguishes the last two.
|
||||
/// Handing the decode-side export to the encoder fails at the first run;
|
||||
/// handing the send-side one to the decoder would feed rANS the wrong
|
||||
/// probabilities, which is the silent-corruption failure mode.
|
||||
///
|
||||
/// Empty for records written before this field existed. Consumers fall back
|
||||
/// to the registry spec, and only then to a filename heuristic.
|
||||
final Map<String, ImageCodecAssetRole> assetRoles;
|
||||
|
||||
/// [kImageCodecBundleVersion] at install time; 0 for a pre-bundle install.
|
||||
final int bundleVersion;
|
||||
|
||||
const ImageCodecModelRecord({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.sourceUrl,
|
||||
required this.localPath,
|
||||
required this.downloadedAt,
|
||||
required this.fileSizeBytes,
|
||||
this.assetFileNames = const [],
|
||||
this.assetRoles = const {},
|
||||
this.bundleVersion = 0,
|
||||
});
|
||||
|
||||
/// The installed file recorded as [role], or null when none was.
|
||||
String? fileNameForRole(ImageCodecAssetRole role) {
|
||||
for (final entry in assetRoles.entries) {
|
||||
if (entry.value == role && assetFileNames.contains(entry.key)) {
|
||||
return entry.key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'source_url': sourceUrl,
|
||||
'local_path': localPath,
|
||||
'downloaded_at': downloadedAt.millisecondsSinceEpoch,
|
||||
'file_size_bytes': fileSizeBytes,
|
||||
'asset_file_names': assetFileNames,
|
||||
// Role NAMES, not ordinals: appending a member to ImageCodecAssetRole
|
||||
// must never re-label files already on disk.
|
||||
'asset_roles': <String, String>{
|
||||
for (final entry in assetRoles.entries) entry.key: entry.value.name,
|
||||
},
|
||||
'bundle_version': bundleVersion,
|
||||
};
|
||||
}
|
||||
|
||||
factory ImageCodecModelRecord.fromJson(Map<String, dynamic> json) {
|
||||
final rawAssets = json['asset_file_names'];
|
||||
final rawRoles = json['asset_roles'];
|
||||
return ImageCodecModelRecord(
|
||||
id: json['id'] as String? ?? '',
|
||||
name: json['name'] as String? ?? '',
|
||||
sourceUrl: json['source_url'] as String? ?? '',
|
||||
localPath: json['local_path'] as String? ?? '',
|
||||
downloadedAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['downloaded_at'] as int? ?? 0,
|
||||
),
|
||||
fileSizeBytes: json['file_size_bytes'] as int? ?? 0,
|
||||
assetFileNames: rawAssets is List
|
||||
? [
|
||||
for (final entry in rawAssets)
|
||||
if (entry is String && entry.isNotEmpty) entry,
|
||||
]
|
||||
: const [],
|
||||
assetRoles: rawRoles is Map
|
||||
? <String, ImageCodecAssetRole>{
|
||||
for (final entry in rawRoles.entries)
|
||||
if (entry.key is String && entry.value is String)
|
||||
entry.key as String: ?parseImageCodecAssetRole(
|
||||
entry.value as String,
|
||||
),
|
||||
}
|
||||
: const {},
|
||||
bundleVersion: json['bundle_version'] as int? ?? 0,
|
||||
);
|
||||
}
|
||||
|
||||
ImageCodecModelRecord copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
String? sourceUrl,
|
||||
String? localPath,
|
||||
DateTime? downloadedAt,
|
||||
int? fileSizeBytes,
|
||||
List<String>? assetFileNames,
|
||||
Map<String, ImageCodecAssetRole>? assetRoles,
|
||||
int? bundleVersion,
|
||||
}) {
|
||||
return ImageCodecModelRecord(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
sourceUrl: sourceUrl ?? this.sourceUrl,
|
||||
localPath: localPath ?? this.localPath,
|
||||
downloadedAt: downloadedAt ?? this.downloadedAt,
|
||||
fileSizeBytes: fileSizeBytes ?? this.fileSizeBytes,
|
||||
assetFileNames: assetFileNames ?? this.assetFileNames,
|
||||
assetRoles: assetRoles ?? this.assetRoles,
|
||||
bundleVersion: bundleVersion ?? this.bundleVersion,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
String imageCodecModelFriendlyName(ImageCodecModelRecord model) {
|
||||
for (final spec in imageCodecPresetModels) {
|
||||
if (spec.id == model.id || spec.fileName == model.name) {
|
||||
return spec.label;
|
||||
}
|
||||
}
|
||||
final trimmed = model.name.trim();
|
||||
if (trimmed.endsWith('.onnx')) {
|
||||
return trimmed.substring(0, trimmed.length - 5);
|
||||
}
|
||||
return trimmed.isEmpty ? model.id : trimmed;
|
||||
}
|
||||
|
||||
/// What a downloaded file *is*, so nothing has to infer it from position or
|
||||
/// filename.
|
||||
///
|
||||
/// This exists because the bundle stopped being "graph, then its data sibling".
|
||||
/// `assets.first` is a decoder graph today and would silently become the
|
||||
/// entropy graph the moment anyone reordered the list; the ONNX session would
|
||||
/// then load 67 MB of entropy model and fail on an input named `y_hat`.
|
||||
enum ImageCodecAssetRole {
|
||||
/// The synthesis graph handed to ORT: `y_hat -> image`.
|
||||
decoderGraph,
|
||||
|
||||
/// `<decoderGraph>.data`. ORT resolves it by the literal filename recorded
|
||||
/// inside the graph, so it MUST sit beside [decoderGraph] under that name.
|
||||
decoderWeights,
|
||||
|
||||
/// The fp32 **send-side** entropy graph: one input `image [1,3,512,512]`,
|
||||
/// outputs `z_q`, `yq0..3`, `sc0..3`. Kept fp32 deliberately — see the
|
||||
/// bit-exactness note in `image_codec_backend.dart`.
|
||||
///
|
||||
/// This is the ENCODE half only. It emits every stage at once, which is valid
|
||||
/// because the encoder already knows `y`; a decoder cannot use it at all.
|
||||
entropyGraph,
|
||||
|
||||
/// Reserved. The 67 MB entropy export is self-contained today and carries no
|
||||
/// external-data sibling; the role exists so a future re-export that does is
|
||||
/// a registry change rather than a schema change.
|
||||
entropyWeights,
|
||||
|
||||
/// The quantised CDF tables the rANS coder indexes. A *wrong* table set
|
||||
/// desynchronises rANS and yields a sharp, plausible, wrong image with no
|
||||
/// error, which is why this asset must carry a SHA-256 before ship.
|
||||
cdfTables,
|
||||
|
||||
/// The fp32 **decode-side** entropy graph, added in bundle version 2.
|
||||
///
|
||||
/// Decoding is inherently sequential — the symbols of stage `i` must be
|
||||
/// decoded before stage `i+1`'s context exists — so the receiver cannot use
|
||||
/// the emit-everything-at-once [entropyGraph]. This export is the same
|
||||
/// sub-networks behind an ONNX `If` on a `stage` selector, so the app can
|
||||
/// call it five times per image:
|
||||
///
|
||||
/// inputs : `z_q [1,128,4,4]`, `base [1,256,16,16]`, `stage int32 [1]`
|
||||
/// outputs: `base0`, `means`, `scales`, all `[1,256,16,16]`
|
||||
///
|
||||
/// `stage < 0` runs the hyper-synthesis branch and fills `base0`;
|
||||
/// `stage 0..3` runs the context branch and fills `means`/`scales`, already
|
||||
/// multiplied by that stage's mask. The branch not taken emits zeros.
|
||||
///
|
||||
/// Appended after [cdfTables] rather than beside [entropyGraph] so no
|
||||
/// existing enum ordinal moves.
|
||||
entropyDecodeGraph,
|
||||
}
|
||||
|
||||
/// Parses a role written by [ImageCodecModelRecord.toJson], or null when the
|
||||
/// string names no role this build knows.
|
||||
///
|
||||
/// Null rather than a default: a record from a NEWER build may name a role that
|
||||
/// does not exist here, and silently filing it under some other role would be
|
||||
/// worse than admitting the file's purpose is unknown.
|
||||
ImageCodecAssetRole? parseImageCodecAssetRole(String value) {
|
||||
for (final role in ImageCodecAssetRole.values) {
|
||||
if (role.name == value) {
|
||||
return role;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// One downloadable file belonging to a model.
|
||||
///
|
||||
/// A model is not always one file. The promoted decoder is an ONNX graph plus an
|
||||
/// external-weights sibling (`<name>.onnx.data`, referenced by relative filename
|
||||
/// from inside the graph), and ONNX Runtime will not load the graph unless the
|
||||
/// sibling sits next to it under exactly that name. Downloading only the 2.8 MB
|
||||
/// graph produces an `ORT_INVALID_PROTOBUF`-class failure at session creation,
|
||||
/// which is indistinguishable from a corrupt download — hence [sha256].
|
||||
class ImageCodecModelAsset {
|
||||
/// Name the file must have on disk. Not derived from the URL: the external
|
||||
/// data reference inside the graph is a literal filename.
|
||||
final String fileName;
|
||||
|
||||
final String sourceUrl;
|
||||
|
||||
/// Expected size in bytes. Advisory — the download re-reads it from the HEAD
|
||||
/// response — but it is what the UI shows before the first byte arrives.
|
||||
final int sizeBytes;
|
||||
|
||||
/// Lowercase hex SHA-256 of the file, or `''` when no digest is known.
|
||||
///
|
||||
/// An empty value **skips verification**; see [hasChecksum]. That is the state
|
||||
/// today, deliberately: nothing has been uploaded, so no digest of a
|
||||
/// *published* file exists and a hard-failing guess would block the feature.
|
||||
final String sha256;
|
||||
|
||||
/// What this file is for. See [ImageCodecAssetRole].
|
||||
final ImageCodecAssetRole role;
|
||||
|
||||
const ImageCodecModelAsset({
|
||||
required this.fileName,
|
||||
required this.sourceUrl,
|
||||
required this.sizeBytes,
|
||||
required this.role,
|
||||
this.sha256 = '',
|
||||
});
|
||||
|
||||
bool get hasChecksum => sha256.length == 64;
|
||||
}
|
||||
|
||||
/// A declared preset model: its identity plus every file it needs on disk.
|
||||
///
|
||||
/// Mirrors the role of `translationPresetModels` (translation_support.dart:117)
|
||||
/// but is a *spec*, not an [ImageCodecModelRecord]. Records describe what is
|
||||
/// actually on this device (path, mtime, real size); specs describe what could
|
||||
/// be fetched. Conflating them is what made the translation stack unable to
|
||||
/// express a two-file model.
|
||||
class ImageCodecModelSpec {
|
||||
final String id;
|
||||
final String label;
|
||||
|
||||
/// Every file the model needs, tagged by [ImageCodecAssetRole]. Downloaded in
|
||||
/// list order into one directory; the order is a download preference only —
|
||||
/// nothing resolves an asset by position.
|
||||
final List<ImageCodecModelAsset> assets;
|
||||
|
||||
/// True while [assets] carry URLs that have never been fetched by anybody.
|
||||
/// The UI must not offer a download for a spec with this set unless the user
|
||||
/// has supplied their own URL. See the note on [imageCodecPresetModels].
|
||||
final bool urlsArePlaceholders;
|
||||
|
||||
/// Which checkpoint these graphs and tables belong to.
|
||||
///
|
||||
/// Tables and graphs are per-checkpoint and are not interchangeable: decoding
|
||||
/// an ft32 bitstream with ft16 tables desynchronises rANS. The bundle's rate
|
||||
/// point is checked against the bitstream's chunk-0 metadata before a decode.
|
||||
final AeicRatePoint ratePoint;
|
||||
|
||||
const ImageCodecModelSpec({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.assets,
|
||||
this.ratePoint = kShippingAeicRatePoint,
|
||||
this.urlsArePlaceholders = false,
|
||||
});
|
||||
|
||||
/// The one asset with [role], or throws when the spec omits it.
|
||||
ImageCodecModelAsset assetFor(ImageCodecAssetRole role) =>
|
||||
assets.firstWhere((asset) => asset.role == role);
|
||||
|
||||
ImageCodecModelAsset? maybeAssetFor(ImageCodecAssetRole role) {
|
||||
for (final asset in assets) {
|
||||
if (asset.role == role) return asset;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/// The decoder graph — the path handed to ONNX Runtime.
|
||||
///
|
||||
/// Defined by role, not by position. Kept under the old name so the settings
|
||||
/// screen and [imageCodecModelFriendlyName] keep working.
|
||||
ImageCodecModelAsset get graph => assetFor(ImageCodecAssetRole.decoderGraph);
|
||||
|
||||
String get fileName => graph.fileName;
|
||||
|
||||
int get totalSizeBytes =>
|
||||
assets.fold<int>(0, (sum, asset) => sum + asset.sizeBytes);
|
||||
|
||||
/// True when this spec describes a bundle that can encode AND decode.
|
||||
///
|
||||
/// All five roles, not four: without [ImageCodecAssetRole.entropyDecodeGraph]
|
||||
/// the install could send a picture and never open one.
|
||||
bool get isComplete =>
|
||||
maybeAssetFor(ImageCodecAssetRole.decoderGraph) != null &&
|
||||
maybeAssetFor(ImageCodecAssetRole.decoderWeights) != null &&
|
||||
maybeAssetFor(ImageCodecAssetRole.entropyGraph) != null &&
|
||||
maybeAssetFor(ImageCodecAssetRole.entropyDecodeGraph) != null &&
|
||||
maybeAssetFor(ImageCodecAssetRole.cdfTables) != null;
|
||||
}
|
||||
|
||||
/// The set of on-disk paths a codec session needs, replacing the bare
|
||||
/// `String modelPath` that used to cross the isolate boundary.
|
||||
///
|
||||
/// [entropyGraphPath], [entropyDecodeGraphPath] and [tablesPath] are nullable
|
||||
/// because a build that was installed before the entropy bundle existed has
|
||||
/// none of them, and that install must keep working for latent synthesis rather
|
||||
/// than becoming unloadable.
|
||||
class ImageCodecBundle {
|
||||
/// `*.onnx`; its `*.onnx.data` sibling must be in the same directory under
|
||||
/// the exact filename the graph records.
|
||||
final String decoderGraphPath;
|
||||
|
||||
/// fp32 **send-side** entropy graph (`image -> z_q, yq*, sc*`), or null on a
|
||||
/// pre-bundle install. Encoding needs this one and only this one.
|
||||
final String? entropyGraphPath;
|
||||
|
||||
/// fp32 **decode-side** entropy graph (`z_q, base, stage -> base0, means,
|
||||
/// scales`), or null on a bundle-version-1 (or pre-bundle) install.
|
||||
///
|
||||
/// Separate from [entropyGraphPath] because they are two different exports of
|
||||
/// the same weights: the send side emits all four stages at once, which a
|
||||
/// decoder cannot use, and a decoder needs the `If`-branched graph it can call
|
||||
/// five times per image. See [ImageCodecAssetRole.entropyDecodeGraph].
|
||||
final String? entropyDecodeGraphPath;
|
||||
|
||||
/// CDF tables binary, or null on a pre-bundle install.
|
||||
final String? tablesPath;
|
||||
|
||||
/// Checkpoint the graphs and tables belong to.
|
||||
final AeicRatePoint ratePoint;
|
||||
|
||||
const ImageCodecBundle({
|
||||
required this.decoderGraphPath,
|
||||
this.entropyGraphPath,
|
||||
this.entropyDecodeGraphPath,
|
||||
this.tablesPath,
|
||||
this.ratePoint = kShippingAeicRatePoint,
|
||||
});
|
||||
|
||||
/// True when this install can run the **send** half of the entropy path.
|
||||
///
|
||||
/// Deliberately does NOT require [entropyDecodeGraphPath]: a version-1 install
|
||||
/// can still encode, and reporting it as unable to do anything would be wrong.
|
||||
/// Use [supportsDecode] to gate a decode.
|
||||
bool get isComplete => entropyGraphPath != null && tablesPath != null;
|
||||
|
||||
/// True when this install can also turn a bitstream back into a picture.
|
||||
bool get supportsDecode => isComplete && entropyDecodeGraphPath != null;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is ImageCodecBundle &&
|
||||
other.decoderGraphPath == decoderGraphPath &&
|
||||
other.entropyGraphPath == entropyGraphPath &&
|
||||
other.entropyDecodeGraphPath == entropyDecodeGraphPath &&
|
||||
other.tablesPath == tablesPath &&
|
||||
other.ratePoint == ratePoint;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
decoderGraphPath,
|
||||
entropyGraphPath,
|
||||
entropyDecodeGraphPath,
|
||||
tablesPath,
|
||||
ratePoint,
|
||||
);
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'ImageCodecBundle(decoder: $decoderGraphPath, entropy: $entropyGraphPath, '
|
||||
'entropyDecode: $entropyDecodeGraphPath, tables: $tablesPath, '
|
||||
'rate: ${ratePoint.name})';
|
||||
}
|
||||
|
||||
/// Published bundle: https://huggingface.co/zjs81/aeic-se-onnx
|
||||
///
|
||||
/// URL shape matches `translation_support.dart`:
|
||||
/// `https://huggingface.co/<repo>/resolve/main/<file>?download=true`.
|
||||
///
|
||||
/// Sizes and digests below are of the published bytes, verified against the
|
||||
/// local exports under `/Users/Zach/Documents/mycode/aic`. The CDF digest is
|
||||
/// not optional: a table set that disagrees with the checkpoint desynchronises
|
||||
/// rANS and produces a sharp, plausible, WRONG image with no error anywhere.
|
||||
///
|
||||
/// EXACTLY ONE ENTRY, and it is ONE bundle rather than a send-only and a
|
||||
/// receive-capable download: encode needs the entropy graph + tables, decode
|
||||
/// needs those *and* the decoder, so splitting them only creates an install
|
||||
/// that can do half the feature.
|
||||
///
|
||||
/// WHY `qdq_conv_pct` AND NOT `qdq_conv_pct_novae`. The `novae` variant leaves
|
||||
/// the VAE decoder in fp32, and the VAE is the only part that runs at full
|
||||
/// 512x512, so its activations dominate: 872 MB on disk and 2.99 GiB peak RSS
|
||||
/// against 835 MB / 2.16 GiB here, for 0.17 dB against the ORIGINAL image.
|
||||
/// 830 MB of phone RAM is the wrong price for that on the platform where RAM,
|
||||
/// not quality, is the binding constraint.
|
||||
final List<ImageCodecModelSpec> imageCodecPresetModels = [
|
||||
const ImageCodecModelSpec(
|
||||
id: 'aeic-se-ft32-bundle-v1',
|
||||
label: 'AEIC-SE 512 image codec (ft32)',
|
||||
ratePoint: AeicRatePoint.ft32,
|
||||
urlsArePlaceholders: false,
|
||||
assets: [
|
||||
ImageCodecModelAsset(
|
||||
role: ImageCodecAssetRole.decoderGraph,
|
||||
fileName: 'aeic_decoder_qdq_conv_pct.onnx',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_decoder_qdq_conv_pct.onnx?download=true',
|
||||
sizeBytes: 3066597,
|
||||
sha256:
|
||||
'fa1ca65c52ecb9e1ec43c05ef792ac8b95ecab21dcb6ab89a825b4dad6a5a571',
|
||||
),
|
||||
ImageCodecModelAsset(
|
||||
role: ImageCodecAssetRole.decoderWeights,
|
||||
fileName: 'aeic_decoder_qdq_conv_pct.onnx.data',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_decoder_qdq_conv_pct.onnx.data?download=true',
|
||||
sizeBytes: 872896480,
|
||||
sha256:
|
||||
'f7714df0ec8cc495be1fb4bad3be0458c186c8a61d87b8487f2e8e6b84b8242a',
|
||||
),
|
||||
ImageCodecModelAsset(
|
||||
role: ImageCodecAssetRole.entropyGraph,
|
||||
fileName: 'aeic_entropy_side_fp32_op17.onnx',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_entropy_side_fp32_op17.onnx?download=true',
|
||||
sizeBytes: 67262167,
|
||||
sha256:
|
||||
'b7b55b0f6a8a02ec2e8f6e85820c064c741c870c226d276c78df45e83ca1a9d6',
|
||||
),
|
||||
// Local digest, for the uploader to confirm the bytes match:
|
||||
// efcbbc4829a0029f487f17b7b52373c6af339d7f74a6417463981d0778d6d444
|
||||
ImageCodecModelAsset(
|
||||
role: ImageCodecAssetRole.entropyDecodeGraph,
|
||||
fileName: 'aeic_entropy_decode_fp32_op17.onnx',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_entropy_decode_fp32_op17.onnx?download=true',
|
||||
sizeBytes: 60509540,
|
||||
sha256:
|
||||
'efcbbc4829a0029f487f17b7b52373c6af339d7f74a6417463981d0778d6d444',
|
||||
),
|
||||
ImageCodecModelAsset(
|
||||
role: ImageCodecAssetRole.cdfTables,
|
||||
fileName: 'aeic_cdf_ft32.bin',
|
||||
sourceUrl:
|
||||
'https://huggingface.co/zjs81/aeic-se-onnx/resolve/main/aeic_cdf_ft32.bin?download=true',
|
||||
sizeBytes: 813648,
|
||||
sha256:
|
||||
'4089fde2af16c340642a5c857be42f6d0f21caf71dd5b4f32d62efcd41c77bd5',
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
|
||||
/// Total download for the one shipping bundle: 1,004,548,432 B (958.0 MiB).
|
||||
///
|
||||
/// Named so the pre-flight space check and the tests share one number instead
|
||||
/// of each re-summing the asset list. Breakdown: 835 MiB decoder (graph +
|
||||
/// external weights), 64.1 MiB send-side entropy graph, 57.7 MiB decode-side
|
||||
/// entropy graph, 0.8 MiB CDF tables.
|
||||
const int kImageCodecBundleTotalBytes = 1004548432;
|
||||
|
||||
/// Result of an encode or a decode.
|
||||
class ImageCodecResult {
|
||||
/// Encode result: the compressed payload only, with no chunk headers.
|
||||
final Uint8List? bitstream;
|
||||
|
||||
/// Decode result: PNG-encoded RGB image.
|
||||
final Uint8List? pngBytes;
|
||||
|
||||
/// Decode result: packed 8-bit RGB, `resolution * resolution * 3` bytes.
|
||||
///
|
||||
/// This is what the inference backend actually produces; [pngBytes] is an
|
||||
/// optional re-encoding of it for widgets that want an `Image.memory`.
|
||||
final Uint8List? rgbBytes;
|
||||
|
||||
final AeicRatePoint ratePoint;
|
||||
|
||||
/// Square edge length. 512 today and a hard floor — the SD-Turbo UNet in the
|
||||
/// decoder needs a 64x64 latent and collapses below it.
|
||||
final int resolution;
|
||||
|
||||
final int durationMs;
|
||||
final ImageCodecStatus status;
|
||||
|
||||
const ImageCodecResult({
|
||||
required this.ratePoint,
|
||||
required this.resolution,
|
||||
required this.durationMs,
|
||||
required this.status,
|
||||
this.bitstream,
|
||||
this.pngBytes,
|
||||
this.rgbBytes,
|
||||
});
|
||||
}
|
||||
|
||||
class ImageCodecDownloadCancelled implements Exception {
|
||||
const ImageCodecDownloadCancelled();
|
||||
|
||||
@override
|
||||
String toString() => 'Download canceled.';
|
||||
}
|
||||
|
||||
/// Raised by the codec session while the native inference seam is unimplemented.
|
||||
class ImageCodecUnimplemented implements Exception {
|
||||
final String detail;
|
||||
|
||||
const ImageCodecUnimplemented(this.detail);
|
||||
|
||||
@override
|
||||
String toString() => 'Image codec backend not implemented: $detail';
|
||||
}
|
||||
|
||||
/// Raised when a bitstream-level [encode]/[decode] is attempted but the entropy
|
||||
/// path is not present in this build.
|
||||
///
|
||||
/// WHY THIS EXISTS AND WHY IT IS NOT A BUG TO FIX HERE. The shipping ONNX
|
||||
/// artifact is the **synthesis half only**: `y_hat [1,256,16,16] -> image
|
||||
/// [1,3,512,512]`. Turning bytes into `y_hat` (and back) needs two more things
|
||||
/// that do not exist in Dart or in the shipped artifact:
|
||||
///
|
||||
/// 1. the entropy-side graph (`h_s`, `g_c` and the four adapters, kept in
|
||||
/// fp32 — `aic/onnx/aeic_entropy_side_fp32_op*.onnx`, 67 MB, not exported
|
||||
/// into the shipped model), and
|
||||
/// 2. the rANS range coder itself, which today exists only as the AEIC C++
|
||||
/// extension (`aic/aeic/src/codec/MLCodec_rans*.so`).
|
||||
///
|
||||
/// Until both land, [ImageCodecBackend.decodeLatentToRgb] is the real, working
|
||||
/// entry point and the bitstream methods throw this. Faking them is not an
|
||||
/// option: an rANS desync produces a sharp, plausible, *wrong* image with no
|
||||
/// error raised, so a stub that returns noise-shaped bytes would be
|
||||
/// indistinguishable from the genuine failure mode this codec already has.
|
||||
class ImageCodecEntropyPathMissing extends ImageCodecUnimplemented {
|
||||
const ImageCodecEntropyPathMissing([
|
||||
super.detail =
|
||||
'the rANS entropy path (entropy-side graph + range coder) is not part '
|
||||
'of this build; only latent->image synthesis is available',
|
||||
]);
|
||||
}
|
||||
|
||||
/// Raised when a bitstream operation is attempted against an install that
|
||||
/// predates the entropy bundle.
|
||||
///
|
||||
/// Distinct from [ImageCodecEntropyPathMissing] on purpose. That one means the
|
||||
/// *build* cannot do this and no user action helps; this one means the *files
|
||||
/// on this device* are a decoder-only install and the remedy is a download.
|
||||
/// Reporting the second as the first is what would strand every user who
|
||||
/// installed the decoder-only build.
|
||||
class ImageCodecBundleIncomplete extends ImageCodecUnimplemented {
|
||||
const ImageCodecBundleIncomplete([
|
||||
super.detail =
|
||||
'the installed model predates the entropy bundle; re-download the '
|
||||
'image codec model',
|
||||
]);
|
||||
}
|
||||
|
||||
/// Raised when a downloaded asset's SHA-256 does not match the expected digest.
|
||||
class ImageCodecIntegrityFailure implements Exception {
|
||||
final String fileName;
|
||||
final String expected;
|
||||
final String actual;
|
||||
|
||||
const ImageCodecIntegrityFailure({
|
||||
required this.fileName,
|
||||
required this.expected,
|
||||
required this.actual,
|
||||
});
|
||||
|
||||
@override
|
||||
String toString() =>
|
||||
'Checksum mismatch for $fileName: expected $expected, got $actual.';
|
||||
}
|
||||
|
||||
/// Persisted image-codec preferences.
|
||||
///
|
||||
/// These five fields are destined for `AppSettings` (see the integration notes
|
||||
/// for task B4, item 1); they live in their own value type until then so this
|
||||
/// workstream adds no edits to `app_settings.dart`. The JSON keys already match
|
||||
/// the `image_codec_*` names the AppSettings migration will use, so moving them
|
||||
/// is a copy, not a rewrite.
|
||||
class ImageCodecPreferences {
|
||||
final bool enabled;
|
||||
final String? selectedModelId;
|
||||
final String? modelSourceUrl;
|
||||
|
||||
/// [AeicRatePoint.wireValue] of the composer's default rate point.
|
||||
final int ratePoint;
|
||||
|
||||
final List<ImageCodecModelRecord> downloadedModels;
|
||||
|
||||
const ImageCodecPreferences({
|
||||
this.enabled = false,
|
||||
this.selectedModelId,
|
||||
this.modelSourceUrl,
|
||||
this.ratePoint = 4, // AeicRatePoint.ft32
|
||||
this.downloadedModels = const [],
|
||||
});
|
||||
|
||||
AeicRatePoint get aeicRatePoint => parseAeicRatePoint(ratePoint);
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'image_codec_enabled': enabled,
|
||||
'image_codec_selected_model_id': selectedModelId,
|
||||
'image_codec_model_source_url': modelSourceUrl,
|
||||
'image_codec_rate_point': ratePoint,
|
||||
'image_codec_downloaded_models': [
|
||||
for (final model in downloadedModels) model.toJson(),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
factory ImageCodecPreferences.fromJson(Map<String, dynamic> json) {
|
||||
final rawModels = json['image_codec_downloaded_models'];
|
||||
return ImageCodecPreferences(
|
||||
enabled: json['image_codec_enabled'] as bool? ?? false,
|
||||
selectedModelId: json['image_codec_selected_model_id'] as String?,
|
||||
modelSourceUrl: json['image_codec_model_source_url'] as String?,
|
||||
ratePoint: json['image_codec_rate_point'] as int? ?? 4,
|
||||
downloadedModels: rawModels is List
|
||||
? [
|
||||
for (final entry in rawModels)
|
||||
if (entry is Map<String, dynamic>)
|
||||
ImageCodecModelRecord.fromJson(entry),
|
||||
]
|
||||
: const [],
|
||||
);
|
||||
}
|
||||
|
||||
static const Object _unset = Object();
|
||||
|
||||
ImageCodecPreferences copyWith({
|
||||
bool? enabled,
|
||||
Object? selectedModelId = _unset,
|
||||
Object? modelSourceUrl = _unset,
|
||||
int? ratePoint,
|
||||
List<ImageCodecModelRecord>? downloadedModels,
|
||||
}) {
|
||||
return ImageCodecPreferences(
|
||||
enabled: enabled ?? this.enabled,
|
||||
selectedModelId: identical(selectedModelId, _unset)
|
||||
? this.selectedModelId
|
||||
: selectedModelId as String?,
|
||||
modelSourceUrl: identical(modelSourceUrl, _unset)
|
||||
? this.modelSourceUrl
|
||||
: modelSourceUrl as String?,
|
||||
ratePoint: ratePoint ?? this.ratePoint,
|
||||
downloadedModels: downloadedModels ?? this.downloadedModels,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -81,6 +81,16 @@ class RadioSettings {
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia (Mid)',
|
||||
RadioSettings(
|
||||
frequencyMHz: 915.075,
|
||||
bandwidth: LoRaBandwidth.bw125,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
(
|
||||
'Australia SA, WA, QLD',
|
||||
RadioSettings(
|
||||
@@ -466,8 +476,8 @@ class RadioSettings {
|
||||
RadioSettings(
|
||||
frequencyMHz: 908.205,
|
||||
bandwidth: LoRaBandwidth.bw62_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf10,
|
||||
codingRate: LoRaCodingRate.cr4_5,
|
||||
spreadingFactor: LoRaSpreadingFactor.sf9,
|
||||
codingRate: LoRaCodingRate.cr4_8,
|
||||
txPowerDbm: 20,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -6,8 +6,11 @@ import 'package:provider/provider.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/image_codec_support.dart';
|
||||
import '../models/translation_support.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/image_codec_service.dart';
|
||||
import '../services/map_tile_cache_service.dart';
|
||||
import '../services/notification_service.dart';
|
||||
import '../services/translation_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
@@ -18,7 +21,16 @@ import '../helpers/snack_bar_builder.dart';
|
||||
import 'map_cache_screen.dart';
|
||||
|
||||
class AppSettingsScreen extends StatelessWidget {
|
||||
const AppSettingsScreen({super.key});
|
||||
/// Scrolls the image-message settings into view once, on first build.
|
||||
///
|
||||
/// This is how the received-image placeholder's "no model installed" tap
|
||||
/// lands somewhere useful: the image block sits below appearance,
|
||||
/// notifications, messaging, battery, map and translation, so pushing the
|
||||
/// plain settings screen would drop the user at the top with no hint that
|
||||
/// the thing they tapped for is several screens down.
|
||||
final bool focusImageMessages;
|
||||
|
||||
const AppSettingsScreen({super.key, this.focusImageMessages = false});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -597,10 +609,206 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
settingsService.setEnableMessageTracing(value);
|
||||
},
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_ScrollIntoViewOnce(
|
||||
enabled: focusImageMessages,
|
||||
child: SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
secondary: const Icon(Icons.image_outlined, size: 20),
|
||||
title: Text(context.l10n.imageMessages_enableTitle),
|
||||
subtitle: Text(context.l10n.imageMessages_enableSubtitle),
|
||||
value: settingsService.settings.imageMessagesEnabled,
|
||||
onChanged: (value) async {
|
||||
// ImageCodecService.availability requires BOTH the app-level
|
||||
// toggle and its own `enabled` preference; one switch drives both
|
||||
// so the user is never left with a silently half-on feature.
|
||||
final codec = _imageCodecService(context);
|
||||
await settingsService.setImageMessagesEnabled(value);
|
||||
await codec?.setEnabled(value);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (settingsService.settings.imageMessagesEnabled) ...[
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
secondary: const Icon(Icons.auto_awesome_outlined, size: 20),
|
||||
title: Text(context.l10n.imageMessages_autoProcessTitle),
|
||||
subtitle: Text(context.l10n.imageMessages_autoProcessSubtitle),
|
||||
value: settingsService.settings.imageProcessAutomatically,
|
||||
onChanged: (value) =>
|
||||
settingsService.setImageProcessAutomatically(value),
|
||||
),
|
||||
_buildImageCodecModels(context),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// The image codec, or null when it is not registered (screen tests).
|
||||
ImageCodecService? _imageCodecService(BuildContext context) {
|
||||
try {
|
||||
return Provider.of<ImageCodecService>(context, listen: false);
|
||||
} on ProviderNotFoundException {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Model registry for image messages: the download entry point, the
|
||||
/// downloaded weights, and — when the build simply cannot run the codec —
|
||||
/// [ImageCodecService.unavailableReason] instead of a dead download button.
|
||||
Widget _buildImageCodecModels(BuildContext context) {
|
||||
final ImageCodecService codec;
|
||||
try {
|
||||
codec = context.watch<ImageCodecService>();
|
||||
} on ProviderNotFoundException {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final reason = codec.unavailableReason;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 4, 16, 16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.imageMessages_modelSectionTitle,
|
||||
style: textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (reason != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: scheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
),
|
||||
child: Text(
|
||||
reason,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
for (final spec in imageCodecPresetModels)
|
||||
_buildImageCodecPreset(context, codec, spec),
|
||||
for (final model in codec.availableModels)
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
leading: const Icon(Icons.check_circle_outline, size: 20),
|
||||
title: Text(model.name.isEmpty ? model.id : model.name),
|
||||
subtitle: Text(_formatModelBytes(model.fileSizeBytes)),
|
||||
trailing: IconButton(
|
||||
icon: const Icon(Icons.delete_outline, size: 20),
|
||||
tooltip: context.l10n.imageMessages_removeModel,
|
||||
onPressed: codec.isBusy || codec.isDownloading
|
||||
? null
|
||||
: () => codec.removeModel(model),
|
||||
),
|
||||
),
|
||||
if (codec.lastError != null) ...[
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
codec.lastError!,
|
||||
style: textTheme.bodySmall?.copyWith(color: scheme.error),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildImageCodecPreset(
|
||||
BuildContext context,
|
||||
ImageCodecService codec,
|
||||
ImageCodecModelSpec spec,
|
||||
) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final alreadyHave = codec.availableModels.any((m) => m.id == spec.id);
|
||||
// downloadPresetModel throws StateError on a placeholder URL, deliberately,
|
||||
// so the button must not be offerable until the weights are published.
|
||||
final blocked = spec.urlsArePlaceholders;
|
||||
final busy = codec.isDownloading;
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ListTile(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
leading: const Icon(Icons.download_outlined, size: 20),
|
||||
title: Text(spec.label),
|
||||
subtitle: Text(
|
||||
blocked
|
||||
? context.l10n.imageMessages_modelNotPublished
|
||||
: _formatModelBytes(spec.totalSizeBytes),
|
||||
),
|
||||
trailing: alreadyHave
|
||||
? Text(
|
||||
context.l10n.imageMessages_modelReady,
|
||||
style: textTheme.labelMedium?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: busy
|
||||
? TextButton(
|
||||
onPressed: codec.cancelDownload,
|
||||
child: Text(context.l10n.imageMessages_cancelDownload),
|
||||
)
|
||||
: TextButton(
|
||||
onPressed: blocked
|
||||
? null
|
||||
: () => _downloadImageCodecModel(context, codec, spec),
|
||||
child: Text(context.l10n.imageMessages_downloadModel),
|
||||
),
|
||||
),
|
||||
if (busy)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: LinearProgressIndicator(value: codec.downloadProgress),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _downloadImageCodecModel(
|
||||
BuildContext context,
|
||||
ImageCodecService codec,
|
||||
ImageCodecModelSpec spec,
|
||||
) async {
|
||||
final messenger = ScaffoldMessenger.of(context);
|
||||
final failed = context.l10n.imageMessages_downloadFailed;
|
||||
try {
|
||||
await codec.downloadPresetModel(spec);
|
||||
} on Exception catch (error) {
|
||||
messenger.showSnackBar(SnackBar(content: Text('$failed $error')));
|
||||
}
|
||||
}
|
||||
|
||||
String _formatModelBytes(int bytes) {
|
||||
if (bytes >= 1024 * 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024 * 1024)).toStringAsFixed(2)} GB';
|
||||
}
|
||||
if (bytes >= 1024 * 1024) {
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(0)} MB';
|
||||
}
|
||||
if (bytes >= 1024) return '${(bytes / 1024).toStringAsFixed(0)} kB';
|
||||
return '$bytes B';
|
||||
}
|
||||
|
||||
Widget _buildBatteryContent(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
@@ -687,6 +895,10 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
value: 'lipo',
|
||||
child: Text(context.l10n.appSettings_batteryLipo),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo_hv',
|
||||
child: Text(context.l10n.appSettings_batteryLipoHv),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -699,52 +911,229 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
return Column(
|
||||
children: [
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
final children = <Widget>[
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
secondary: const Icon(Icons.router_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showRepeaters),
|
||||
subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle),
|
||||
value: settingsService.settings.mapShowRepeaters,
|
||||
onChanged: (value) => settingsService.setMapShowRepeaters(value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
secondary: const Icon(Icons.chat_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showChatNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle),
|
||||
value: settingsService.settings.mapShowChatNodes,
|
||||
onChanged: (value) => settingsService.setMapShowChatNodes(value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
secondary: const Icon(Icons.people_outline, size: 20),
|
||||
title: Text(context.l10n.appSettings_showOtherNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle),
|
||||
value: settingsService.settings.mapShowOtherNodes,
|
||||
onChanged: (value) => settingsService.setMapShowOtherNodes(value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showTimeFilterSheet(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_timeFilter,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.mapTimeFilterHours == 0
|
||||
? context.l10n.appSettings_timeFilterShowAll
|
||||
: context.l10n.appSettings_timeFilterShowLast(
|
||||
settingsService.settings.mapTimeFilterHours
|
||||
.toInt(),
|
||||
),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
secondary: const Icon(Icons.router_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showRepeaters),
|
||||
subtitle: Text(context.l10n.appSettings_showRepeatersSubtitle),
|
||||
value: settingsService.settings.mapShowRepeaters,
|
||||
onChanged: (value) => settingsService.setMapShowRepeaters(value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showUnitsSheet(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.straighten, size: 20, color: scheme.onSurfaceVariant),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_unitsTitle,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.unitSystem == UnitSystem.imperial
|
||||
? context.l10n.appSettings_unitsImperial
|
||||
: context.l10n.appSettings_unitsMetric,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
secondary: const Icon(Icons.chat_outlined, size: 20),
|
||||
title: Text(context.l10n.appSettings_showChatNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showChatNodesSubtitle),
|
||||
value: settingsService.settings.mapShowChatNodes,
|
||||
onChanged: (value) => settingsService.setMapShowChatNodes(value),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
SwitchListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 4,
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MapCacheScreen()),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.download_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_offlineMapCache,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.mapCacheBounds == null
|
||||
? context.l10n.appSettings_noAreaSelected
|
||||
: context.l10n.appSettings_areaSelectedZoom(
|
||||
settingsService.settings.mapCacheMinZoom,
|
||||
settingsService.settings.mapCacheMaxZoom,
|
||||
),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
secondary: const Icon(Icons.people_outline, size: 20),
|
||||
title: Text(context.l10n.appSettings_showOtherNodes),
|
||||
subtitle: Text(context.l10n.appSettings_showOtherNodesSubtitle),
|
||||
value: settingsService.settings.mapShowOtherNodes,
|
||||
onChanged: (value) => settingsService.setMapShowOtherNodes(value),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showMapRasterSourceDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.layers_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_rasterTileSource,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_mapRasterSourceSummary(settingsService.settings),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
if (_isStadiaSource(settingsService.settings)) {
|
||||
children.addAll([
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showTimeFilterSheet(context, settingsService),
|
||||
onTap: () => _showMapRasterEndpointDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.timer_outlined,
|
||||
Icons.public_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -754,19 +1143,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_timeFilter,
|
||||
context.l10n.appSettings_stadiaEndpoint,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.mapTimeFilterHours == 0
|
||||
? context.l10n.appSettings_timeFilterShowAll
|
||||
: context.l10n.appSettings_timeFilterShowLast(
|
||||
settingsService.settings.mapTimeFilterHours
|
||||
.toInt(),
|
||||
),
|
||||
_mapRasterEndpointSummary(settingsService.settings),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -785,13 +1169,13 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () => _showUnitsSheet(context, settingsService),
|
||||
onTap: () => _showMapApiKeyDialog(context, settingsService),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.straighten,
|
||||
Icons.key_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -801,17 +1185,14 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_unitsTitle,
|
||||
context.l10n.appSettings_stadiaApiKey,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.unitSystem ==
|
||||
UnitSystem.imperial
|
||||
? context.l10n.appSettings_unitsImperial
|
||||
: context.l10n.appSettings_unitsMetric,
|
||||
_mapApiKeySummary(context, settingsService.settings),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
@@ -828,59 +1209,208 @@ class AppSettingsScreen extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const MapCacheScreen()),
|
||||
);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.download_outlined,
|
||||
size: 20,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
]);
|
||||
}
|
||||
|
||||
return Column(children: children);
|
||||
}
|
||||
|
||||
String _mapRasterSourceSummary(AppSettings settings) {
|
||||
final source = MapRasterSourceCatalog.fromSettings(settings);
|
||||
return '${source.label} - ${source.description}';
|
||||
}
|
||||
|
||||
bool _isStadiaSource(AppSettings settings) {
|
||||
return MapRasterSourceCatalog.fromSettings(settings).isStadia;
|
||||
}
|
||||
|
||||
String _mapRasterEndpointSummary(AppSettings settings) {
|
||||
final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
|
||||
return '${endpoint.label} - ${endpoint.description}';
|
||||
}
|
||||
|
||||
String _mapApiKeySummary(BuildContext context, AppSettings settings) {
|
||||
return context.l10n.appSettings_stadiaApiKeyConfigured(
|
||||
_maskApiKey(settings.effectiveMapTileApiKey),
|
||||
);
|
||||
}
|
||||
|
||||
String _maskApiKey(String value) {
|
||||
if (value.length <= 8) return '********';
|
||||
return '${value.substring(0, 4)}...${value.substring(value.length - 4)}';
|
||||
}
|
||||
|
||||
void _showMapRasterSourceDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
String selectedId = settingsService.settings.mapRasterSourceId;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_rasterTileSource),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: MediaQuery.of(dialogContext).size.height * 0.6,
|
||||
),
|
||||
child: SingleChildScrollView(
|
||||
child: RadioGroup<String>(
|
||||
groupValue: selectedId,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
selectedId = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
context.l10n.appSettings_offlineMapCache,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
for (final preset in MapRasterSourcePreset.values)
|
||||
Builder(
|
||||
builder: (context) {
|
||||
final option = MapRasterSourceCatalog.fromPreset(
|
||||
preset,
|
||||
);
|
||||
return RadioListTile<String>(
|
||||
value: preset.id,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.description),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
settingsService.settings.mapCacheBounds == null
|
||||
? context.l10n.appSettings_noAreaSelected
|
||||
: context.l10n.appSettings_areaSelectedZoom(
|
||||
settingsService.settings.mapCacheMinZoom,
|
||||
settingsService.settings.mapCacheMaxZoom,
|
||||
),
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
color: scheme.onSurfaceVariant,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await settingsService.setMapRasterSourceId(selectedId);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMapRasterEndpointDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
String selectedId = settingsService.settings.mapTileEndpointId;
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (dialogContext, setState) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_stadiaEndpoint),
|
||||
content: SizedBox(
|
||||
width: 360,
|
||||
child: RadioGroup<String>(
|
||||
groupValue: selectedId,
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setState(() {
|
||||
selectedId = value;
|
||||
});
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
for (final option in MapRasterEndpointCatalog.presets)
|
||||
RadioListTile<String>(
|
||||
value: option.id,
|
||||
title: Text(option.label),
|
||||
subtitle: Text(option.description),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await settingsService.setMapTileEndpointId(selectedId);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showMapApiKeyDialog(
|
||||
BuildContext context,
|
||||
AppSettingsService settingsService,
|
||||
) {
|
||||
final currentApiKey = settingsService.settings.mapTileApiKey?.trim() ?? '';
|
||||
final maskedApiKey = _maskApiKey(
|
||||
currentApiKey.isEmpty ? AppSettings.stadiaDemo : currentApiKey,
|
||||
);
|
||||
final controller = TextEditingController(text: maskedApiKey);
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.appSettings_stadiaApiKey),
|
||||
content: SizedBox(
|
||||
width: 420,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(context.l10n.appSettings_stadiaApiKeyDialogDescription),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
autofillHints: const [AutofillHints.password],
|
||||
decoration: const InputDecoration(
|
||||
border: OutlineInputBorder(),
|
||||
hintText: '4e1bf343-3d91-4d9c-a8e1-1234567890ab',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final apiKey = controller.text.trim();
|
||||
await settingsService.setMapTileApiKey(
|
||||
apiKey == maskedApiKey ? currentApiKey : apiKey,
|
||||
);
|
||||
if (!dialogContext.mounted) return;
|
||||
Navigator.pop(dialogContext);
|
||||
},
|
||||
child: Text(context.l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2186,3 +2716,45 @@ class _TranslationLanguageDialogContentState
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Scrolls [child] into view exactly once, after the first frame.
|
||||
///
|
||||
/// A `StatefulWidget` rather than a post-frame callback in `build` because the
|
||||
/// settings screen rebuilds on every `AppSettingsService` notification: a
|
||||
/// callback in `build` would yank the list back to the image block every time
|
||||
/// the user touched an unrelated switch.
|
||||
///
|
||||
/// Silently does nothing when there is no enclosing [Scrollable] (screen tests
|
||||
/// that render a section in isolation) or when the element is gone by the time
|
||||
/// the frame lands.
|
||||
class _ScrollIntoViewOnce extends StatefulWidget {
|
||||
final bool enabled;
|
||||
final Widget child;
|
||||
|
||||
const _ScrollIntoViewOnce({required this.enabled, required this.child});
|
||||
|
||||
@override
|
||||
State<_ScrollIntoViewOnce> createState() => _ScrollIntoViewOnceState();
|
||||
}
|
||||
|
||||
class _ScrollIntoViewOnceState extends State<_ScrollIntoViewOnce> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (!widget.enabled) return;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted) return;
|
||||
final scrollable = Scrollable.maybeOf(context);
|
||||
if (scrollable == null) return;
|
||||
Scrollable.ensureVisible(
|
||||
context,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
alignment: 0.1,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => widget.child;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,13 @@ import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
enum _BleLogView { frames, rawLogRx }
|
||||
|
||||
int _decodeRawPathByteLen(int pathLenRaw) {
|
||||
if (pathLenRaw == 0xFF || pathLenRaw == 0) return 0;
|
||||
final hashCount = pathLenRaw & 0x3F;
|
||||
final hashWidth = ((pathLenRaw >> 6) & 0x03) + 1;
|
||||
return hashCount * hashWidth;
|
||||
}
|
||||
|
||||
class BleDebugLogScreen extends StatefulWidget {
|
||||
const BleDebugLogScreen({super.key});
|
||||
|
||||
@@ -110,20 +117,32 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final entry = entries[index];
|
||||
final time =
|
||||
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
|
||||
return GestureDetector(
|
||||
onLongPress: () async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: entry.payload
|
||||
.map(
|
||||
(b) => b
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0'),
|
||||
)
|
||||
.join(''),
|
||||
Future<void> copyHex() async {
|
||||
await Clipboard.setData(
|
||||
ClipboardData(
|
||||
text: entry.payload
|
||||
.map(
|
||||
(b) => b
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0'),
|
||||
)
|
||||
.join(''),
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.debugLog_bleCopied,
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: copyHex,
|
||||
onLongPress: copyHex,
|
||||
onSecondaryTap: copyHex,
|
||||
child: Container(
|
||||
color: MeshPalette.bg,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
@@ -302,16 +321,17 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
rawHex: _bytesToHex(raw),
|
||||
);
|
||||
}
|
||||
final pathLen = raw[index++];
|
||||
if (raw.length < index + pathLen) {
|
||||
final pathLenRaw = raw[index++];
|
||||
final pathByteLen = _decodeRawPathByteLen(pathLenRaw);
|
||||
if (raw.length < index + pathByteLen) {
|
||||
return _RawPacketInfo(
|
||||
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
|
||||
summary: 'Truncated path',
|
||||
rawHex: _bytesToHex(raw),
|
||||
);
|
||||
}
|
||||
final pathBytes = raw.sublist(index, index + pathLen);
|
||||
index += pathLen;
|
||||
final pathBytes = raw.sublist(index, index + pathByteLen);
|
||||
index += pathByteLen;
|
||||
if (raw.length <= index) {
|
||||
return _RawPacketInfo(
|
||||
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
|
||||
@@ -324,7 +344,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
|
||||
final title =
|
||||
'RX ${_payloadTypeLabel(payloadType)} • ${_routeLabel(routeType)} • v$payloadVer';
|
||||
final summary = _decodePayloadSummary(payloadType, payload);
|
||||
final pathSummary = pathLen > 0
|
||||
final pathSummary = pathByteLen > 0
|
||||
? 'Path=${_bytesToHex(pathBytes)}'
|
||||
: 'Path=none';
|
||||
final detail = '$summary • $pathSummary • len=${raw.length}';
|
||||
|
||||
@@ -4,6 +4,7 @@ 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/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -22,7 +23,6 @@ import '../theme/mesh_theme.dart';
|
||||
import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/path_map_ui.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class ChannelMessagePathScreen extends StatelessWidget {
|
||||
final ChannelMessage message;
|
||||
@@ -43,19 +43,40 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
message.pathVariants,
|
||||
);
|
||||
|
||||
final primaryPath = !channelMessage && !message.isOutgoing
|
||||
? Uint8List.fromList(primaryPathTmp.reversed.toList())
|
||||
: primaryPathTmp;
|
||||
final hashByteWidth =
|
||||
(message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final primaryPath = _orientPathBytes(
|
||||
primaryPathTmp,
|
||||
hashByteWidth,
|
||||
reverse: !channelMessage && !message.isOutgoing,
|
||||
);
|
||||
final hops = _buildPathHops(
|
||||
primaryPath,
|
||||
connector,
|
||||
l10n,
|
||||
hashByteWidth,
|
||||
resolveFromEnd: !message.isOutgoing,
|
||||
);
|
||||
final hasHopDetails = primaryPath.isNotEmpty;
|
||||
final observedLabel = _formatObservedHops(
|
||||
|
||||
// Convert observed path byte length to hop count using the packet width.
|
||||
// Legacy messages fall back to the current connector width.
|
||||
// Reported path length (V3+) is already stored as a hop count; preserve
|
||||
// the negative flood sentinel when present.
|
||||
final observedHopCount = _hopCountFromBytes(
|
||||
primaryPath.length,
|
||||
message.pathLength,
|
||||
hashByteWidth,
|
||||
);
|
||||
final reportedHopCount = message.pathLength;
|
||||
final effectiveHopCount = observedHopCount > 0
|
||||
? observedHopCount
|
||||
: reportedHopCount;
|
||||
|
||||
final observedLabel = _formatObservedHops(
|
||||
observedHopCount,
|
||||
effectiveHopCount,
|
||||
l10n,
|
||||
);
|
||||
final extraPaths = _otherPaths(primaryPath, message.pathVariants);
|
||||
@@ -73,11 +94,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
title: context.l10n.contacts_repeaterPathTrace,
|
||||
path: primaryPath,
|
||||
flipPathAround: true,
|
||||
reversePathAround:
|
||||
!(!channelMessage && !message.isOutgoing),
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
reversePathAround: false,
|
||||
pathHashByteWidth: hashByteWidth,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -98,13 +116,20 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
children: [
|
||||
_buildSummaryCard(context, observedLabel: observedLabel),
|
||||
_buildSummaryCard(
|
||||
context,
|
||||
observedLabel: observedLabel,
|
||||
effectiveHopCount: effectiveHopCount,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (extraPaths.isNotEmpty) ...[
|
||||
SectionHeader(
|
||||
l10n.channelPath_otherObservedPaths,
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||
),
|
||||
_buildPathVariants(context, extraPaths),
|
||||
const SizedBox(height: 8),
|
||||
_buildPathVariants(context, extraPaths, hashByteWidth),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
SectionHeader(
|
||||
l10n.channelPath_repeaterHops,
|
||||
@@ -123,14 +148,18 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSummaryCard(BuildContext context, {String? observedLabel}) {
|
||||
Widget _buildSummaryCard(
|
||||
BuildContext context, {
|
||||
String? observedLabel,
|
||||
required int? effectiveHopCount,
|
||||
}) {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final routeChip = message.pathLength == null
|
||||
final routeChip = effectiveHopCount == null
|
||||
? null
|
||||
: message.pathLength! < 0
|
||||
: effectiveHopCount < 0
|
||||
? const RouteChip(isDirect: false)
|
||||
: RouteChip(isDirect: true, hops: message.pathLength);
|
||||
: RouteChip(isDirect: true, hops: effectiveHopCount);
|
||||
|
||||
return MeshCard(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
@@ -172,7 +201,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
_buildDetailRow(
|
||||
context,
|
||||
l10n.channelPath_pathLabelTitle,
|
||||
_formatPathLabel(message.pathLength, l10n),
|
||||
_formatPathLabel(effectiveHopCount, l10n),
|
||||
scheme: scheme,
|
||||
),
|
||||
if (observedLabel != null)
|
||||
@@ -187,7 +216,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPathVariants(BuildContext context, List<Uint8List> variants) {
|
||||
Widget _buildPathVariants(
|
||||
BuildContext context,
|
||||
List<Uint8List> variants,
|
||||
int hashByteWidth,
|
||||
) {
|
||||
final l10n = context.l10n;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -210,7 +243,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
Text(
|
||||
l10n.channelPath_observedPathTitle(
|
||||
i + 1,
|
||||
_formatHopCount(variants[i].length, l10n),
|
||||
_formatHopCount(
|
||||
variants[i].length,
|
||||
hashByteWidth,
|
||||
l10n,
|
||||
),
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
@@ -218,7 +255,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
_formatPathPrefixes(variants[i]),
|
||||
_formatPathPrefixes(variants[i], hashByteWidth),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@@ -402,31 +439,31 @@ class ChannelMessagePathScreen extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _formatPathLabel(int? pathLength, AppLocalizations l10n) {
|
||||
if (pathLength == null) return l10n.channelPath_unknownPath;
|
||||
if (pathLength < 0) return l10n.channelPath_floodPath;
|
||||
if (pathLength == 0) return l10n.channelPath_directPath;
|
||||
return l10n.chat_hopsCount(pathLength);
|
||||
String _formatPathLabel(int? hopCount, AppLocalizations l10n) {
|
||||
if (hopCount == null) return l10n.channelPath_unknownPath;
|
||||
if (hopCount < 0) return l10n.channelPath_floodPath;
|
||||
if (hopCount == 0) return l10n.channelPath_directPath;
|
||||
return l10n.chat_hopsCount(hopCount);
|
||||
}
|
||||
|
||||
String? _formatObservedHops(
|
||||
int observedCount,
|
||||
int? pathLength,
|
||||
int? targetHopCount,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
if (observedCount <= 0 && (pathLength == null || pathLength <= 0)) {
|
||||
if (observedCount <= 0 && (targetHopCount == null || targetHopCount <= 0)) {
|
||||
return null;
|
||||
}
|
||||
if (pathLength == null || pathLength < 0) {
|
||||
if (targetHopCount == null || targetHopCount < 0) {
|
||||
return observedCount > 0 ? l10n.chat_hopsCount(observedCount) : null;
|
||||
}
|
||||
if (observedCount == 0) {
|
||||
return l10n.channelPath_observedZeroOf(pathLength);
|
||||
return l10n.channelPath_observedZeroOf(targetHopCount);
|
||||
}
|
||||
if (observedCount == pathLength) {
|
||||
if (observedCount == targetHopCount) {
|
||||
return l10n.chat_hopsCount(observedCount);
|
||||
}
|
||||
return l10n.channelPath_observedSomeOf(observedCount, pathLength);
|
||||
return l10n.channelPath_observedSomeOf(observedCount, targetHopCount);
|
||||
}
|
||||
|
||||
Widget _buildDetailRow(
|
||||
@@ -623,7 +660,10 @@ class _ChannelMessagePathMapScreenState
|
||||
? kPrimaryPathColor
|
||||
: kAlternatePathColors[(index - 1) % kAlternatePathColors.length],
|
||||
isPrimary: isPrimary,
|
||||
hopBytes: List<int>.from(orientedBytes),
|
||||
hopBytes: [
|
||||
for (final hop in hops)
|
||||
if (hop.hopBytes != null) Uint8List.fromList(hop.hopBytes!),
|
||||
],
|
||||
points: points,
|
||||
pointLabels: labels,
|
||||
pointConfirmed: confirmed,
|
||||
@@ -808,7 +848,11 @@ class _ChannelMessagePathMapScreenState
|
||||
primaryPath,
|
||||
);
|
||||
|
||||
final selectedPath = _orientPath(selectedPathTmp);
|
||||
final width =
|
||||
(widget.message.pathHashWidth ?? connector.pathHashByteWidth)
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final selectedPath = _orientPath(selectedPathTmp, width);
|
||||
|
||||
// Match on the unoriented bytes — observedPaths stores them as
|
||||
// recorded, while selectedPath may be reversed for display.
|
||||
@@ -817,19 +861,21 @@ class _ChannelMessagePathMapScreenState
|
||||
selectedPath,
|
||||
connector,
|
||||
context.l10n,
|
||||
width,
|
||||
resolveFromEnd: !widget.message.isOutgoing,
|
||||
);
|
||||
|
||||
// Renderable paths for the animation and combined view.
|
||||
final entries = <_ObservedPathEntry>[];
|
||||
for (var i = 0; i < observedPaths.length; i++) {
|
||||
final oriented = _orientPath(observedPaths[i].pathBytes);
|
||||
final oriented = _orientPath(observedPaths[i].pathBytes, width);
|
||||
final pathHops = i == selectedIndex
|
||||
? hops
|
||||
: _buildPathHops(
|
||||
oriented,
|
||||
connector,
|
||||
context.l10n,
|
||||
width,
|
||||
resolveFromEnd: !widget.message.isOutgoing,
|
||||
);
|
||||
final display = _buildDisplayPath(
|
||||
@@ -908,7 +954,7 @@ class _ChannelMessagePathMapScreenState
|
||||
? LatLngBounds.fromPoints(points)
|
||||
: null;
|
||||
final mapKey = ValueKey(
|
||||
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}',
|
||||
'${_formatPathPrefixes(selectedPath, width)},${context.l10n.pathTrace_you}',
|
||||
);
|
||||
_pathDistance = _getPathDistance(points);
|
||||
|
||||
@@ -967,7 +1013,7 @@ class _ChannelMessagePathMapScreenState
|
||||
},
|
||||
),
|
||||
children: [
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
tileCache.buildTileLayer(context),
|
||||
AnimatedBuilder(
|
||||
animation: _playback,
|
||||
builder: (context, _) {
|
||||
@@ -1045,6 +1091,7 @@ class _ChannelMessagePathMapScreenState
|
||||
context,
|
||||
observedPaths,
|
||||
selectedIndex,
|
||||
width,
|
||||
(index) {
|
||||
setState(() {
|
||||
_selectedPath = observedPaths[index].pathBytes;
|
||||
@@ -1086,10 +1133,12 @@ class _ChannelMessagePathMapScreenState
|
||||
BuildContext context,
|
||||
List<_ObservedPath> paths,
|
||||
int selectedIndex,
|
||||
int hashByteWidth,
|
||||
ValueChanged<int> onSelected, {
|
||||
double topOffset = 16,
|
||||
}) {
|
||||
final l10n = context.l10n;
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final selectedPath = paths[selectedIndex];
|
||||
final label = selectedPath.isPrimary
|
||||
? l10n.channelPath_primaryPath(selectedIndex + 1)
|
||||
@@ -1120,7 +1169,7 @@ class _ChannelMessagePathMapScreenState
|
||||
value: i,
|
||||
child: Text(
|
||||
'${paths[i].isPrimary ? l10n.channelPath_primaryPath(i + 1) : l10n.channelPath_pathLabel(i + 1)}'
|
||||
' • ${_formatHopCount(paths[i].pathBytes.length, l10n)}',
|
||||
' • ${_formatHopCount(paths[i].pathBytes.length, width, l10n)}',
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1134,7 +1183,7 @@ class _ChannelMessagePathMapScreenState
|
||||
Text(
|
||||
l10n.channelPath_selectedPathLabel(
|
||||
label,
|
||||
_formatPathPrefixes(selectedPath.pathBytes),
|
||||
_formatPathPrefixes(selectedPath.pathBytes, width),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
@@ -1378,11 +1427,11 @@ class _ChannelMessagePathMapScreenState
|
||||
}
|
||||
|
||||
/// Orients recorded path bytes in the direction the packet traveled.
|
||||
Uint8List _orientPath(Uint8List bytes) {
|
||||
Uint8List _orientPath(Uint8List bytes, int hashByteWidth) {
|
||||
final reverse =
|
||||
(!widget.message.isOutgoing && !widget.channelMessage) ||
|
||||
(widget.message.isOutgoing && widget.channelMessage);
|
||||
return reverse ? Uint8List.fromList(bytes.reversed.toList()) : bytes;
|
||||
return _orientPathBytes(bytes, hashByteWidth, reverse: reverse);
|
||||
}
|
||||
|
||||
Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
|
||||
@@ -1738,6 +1787,7 @@ class _PathHop {
|
||||
final Contact? contact;
|
||||
final LatLng? position;
|
||||
final AppLocalizations l10n;
|
||||
final Uint8List? hopBytes;
|
||||
|
||||
const _PathHop({
|
||||
required this.index,
|
||||
@@ -1745,12 +1795,18 @@ class _PathHop {
|
||||
required this.contact,
|
||||
required this.position,
|
||||
required this.l10n,
|
||||
this.hopBytes,
|
||||
});
|
||||
|
||||
bool get hasLocation => position != null;
|
||||
|
||||
String get displayLabel {
|
||||
final prefixLabel = _formatPrefix(prefix);
|
||||
final prefixLabel = hopBytes != null && hopBytes!.isNotEmpty
|
||||
? hopBytes!
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join('')
|
||||
.toUpperCase()
|
||||
: _formatPrefix(prefix);
|
||||
return '($prefixLabel) ${_resolveName(contact, l10n)}';
|
||||
}
|
||||
}
|
||||
@@ -1765,10 +1821,12 @@ class _ObservedPath {
|
||||
List<_PathHop> _buildPathHops(
|
||||
Uint8List pathBytes,
|
||||
MeshCoreConnector connector,
|
||||
AppLocalizations l10n, {
|
||||
AppLocalizations l10n,
|
||||
int hashByteWidth, {
|
||||
bool resolveFromEnd = false,
|
||||
}) {
|
||||
if (pathBytes.isEmpty) return const [];
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
final endpoint =
|
||||
(connector.selfLatitude != null && connector.selfLongitude != null)
|
||||
? LatLng(connector.selfLatitude!, connector.selfLongitude!)
|
||||
@@ -1778,19 +1836,23 @@ List<_PathHop> _buildPathHops(
|
||||
contacts: connector.allContacts,
|
||||
endpoint: endpoint,
|
||||
resolveFromEnd: resolveFromEnd,
|
||||
pathHashByteWidth: width,
|
||||
);
|
||||
|
||||
final hopChunks = PathHelper.splitPathBytes(pathBytes, width);
|
||||
final hops = <_PathHop>[];
|
||||
for (var i = 0; i < pathBytes.length; i++) {
|
||||
final contact = resolvedContacts[i];
|
||||
for (var i = 0; i < hopChunks.length; i++) {
|
||||
final hopBytes = hopChunks[i];
|
||||
final contact = i < resolvedContacts.length ? resolvedContacts[i] : null;
|
||||
final resolvedPosition = _resolvePosition(contact);
|
||||
hops.add(
|
||||
_PathHop(
|
||||
index: i + 1,
|
||||
prefix: pathBytes[i],
|
||||
prefix: hopBytes.isNotEmpty ? hopBytes[0] : 0,
|
||||
contact: contact,
|
||||
position: resolvedPosition,
|
||||
l10n: l10n,
|
||||
hopBytes: hopBytes,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1810,14 +1872,35 @@ String _formatPrefix(int prefix) {
|
||||
return prefix.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
}
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
String _formatPathPrefixes(Uint8List pathBytes, int hashByteWidth) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
String _formatHopCount(int count, AppLocalizations l10n) {
|
||||
return l10n.chat_hopsCount(count);
|
||||
Uint8List _orientPathBytes(
|
||||
Uint8List pathBytes,
|
||||
int hashByteWidth, {
|
||||
required bool reverse,
|
||||
}) {
|
||||
if (!reverse || pathBytes.isEmpty) return pathBytes;
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, hashByteWidth);
|
||||
return Uint8List.fromList([for (final hop in hops.reversed) ...hop]);
|
||||
}
|
||||
|
||||
int _hopCountFromBytes(int byteCount, int hashByteWidth) {
|
||||
if (byteCount <= 0) return 0;
|
||||
final width = hashByteWidth.clamp(1, 4).toInt();
|
||||
return (byteCount + width - 1) ~/ width;
|
||||
}
|
||||
|
||||
String _formatHopCount(
|
||||
int byteCount,
|
||||
int hashByteWidth,
|
||||
AppLocalizations l10n,
|
||||
) {
|
||||
return l10n.chat_hopsCount(_hopCountFromBytes(byteCount, hashByteWidth));
|
||||
}
|
||||
|
||||
String _resolveName(Contact? contact, AppLocalizations l10n) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/storage/channel_message_store.dart';
|
||||
import 'package:meshcore_open/utils/keys.dart';
|
||||
import 'package:meshcore_open/utils/platform_info.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -27,6 +27,7 @@ import '../widgets/qr_code_display.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/unread_badge.dart';
|
||||
import '../helpers/gif_helper.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import 'channel_chat_screen.dart';
|
||||
import 'community_qr_scanner_screen.dart';
|
||||
@@ -282,8 +283,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
buildDefaultDragHandles: false,
|
||||
itemCount: filteredChannels.length,
|
||||
onReorderItem: (oldIndex, newIndex) {
|
||||
// onReorderItem already adjusts newIndex after the
|
||||
// removed item, unlike the deprecated onReorder.
|
||||
final reordered = List<Channel>.from(
|
||||
filteredChannels,
|
||||
);
|
||||
@@ -374,13 +373,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
_communityIndex,
|
||||
);
|
||||
final bool isCommunityChannel = Channel.isCommunityChannel(channelType);
|
||||
final community = isCommunityChannel
|
||||
? _communityIndex.getCommunityForChannel(channel)
|
||||
: null;
|
||||
// Only flood-routed channels carry a region; show it when one is set.
|
||||
String subtitle = connector.hasChannelRegion(channel.index)
|
||||
? context.l10n.channels_regionSetTo(
|
||||
connector.getChannelRegion(channel.index),
|
||||
)
|
||||
: '';
|
||||
switch (channelType) {
|
||||
case ChannelType.communityPublic:
|
||||
icon = Icons.groups;
|
||||
iconColor = MeshPalette.magenta;
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_publicChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.communityHashtag:
|
||||
icon = Icons.groups;
|
||||
iconColor = MeshPalette.magenta;
|
||||
if (community != null) {
|
||||
subtitle =
|
||||
'${context.l10n.community_hashtagChannel} • ${community.name}';
|
||||
}
|
||||
case ChannelType.public:
|
||||
icon = Icons.public;
|
||||
iconColor = MeshPalette.signal;
|
||||
@@ -395,7 +411,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
// Last message preview
|
||||
final messages = connector.getChannelMessages(channel);
|
||||
final lastMessage = messages.isNotEmpty ? messages.last : null;
|
||||
final lastPreview = lastMessage?.text ?? '';
|
||||
final lastMessageText = lastMessage?.text ?? '';
|
||||
final lastPreview =
|
||||
lastMessageText.isNotEmpty &&
|
||||
GifHelper.parseGif(lastMessageText) != null
|
||||
? context.l10n.chat_receivedGif
|
||||
: lastMessageText;
|
||||
final lastTime = lastMessage?.timestamp;
|
||||
|
||||
final channelLabel = channel.name.isEmpty
|
||||
@@ -429,142 +450,156 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
channelMessageStore,
|
||||
channel,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onSecondaryTapUp: PlatformInfo.isDesktop
|
||||
? (_) => _showChannelActions(
|
||||
this.context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
// Leading avatar with optional community badge
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
AvatarCircle(
|
||||
name: channelLabel,
|
||||
size: 42,
|
||||
color: iconColor,
|
||||
icon: icon,
|
||||
),
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: MeshPalette.magenta,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.people,
|
||||
size: 8,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Title + subtitle + ch chip
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
channelLabel,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
StatusChip(
|
||||
label: 'CH ${channel.index}',
|
||||
color: MeshPalette.blue,
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
),
|
||||
if (lastPreview.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
lastPreview,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? () => _showChannelActions(
|
||||
this.context,
|
||||
connector,
|
||||
channelMessageStore,
|
||||
channel,
|
||||
)
|
||||
: null,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Leading avatar with optional community badge
|
||||
Stack(
|
||||
clipBehavior: Clip.none,
|
||||
children: [
|
||||
AvatarCircle(
|
||||
name: channelLabel,
|
||||
size: 42,
|
||||
color: iconColor,
|
||||
icon: icon,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Right side: time + unread badge + muted + drag handle
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
if (isCommunityChannel)
|
||||
Positioned(
|
||||
right: -2,
|
||||
bottom: -2,
|
||||
child: Container(
|
||||
width: 16,
|
||||
height: 16,
|
||||
decoration: BoxDecoration(
|
||||
color: MeshPalette.magenta,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.surfaceContainerLow,
|
||||
width: 2,
|
||||
),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.people,
|
||||
size: 8,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// Title + subtitle + ch chip
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (lastTime != null)
|
||||
Text(
|
||||
_relativeTime(lastTime),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
if (isMuted) ...[
|
||||
Icon(
|
||||
Icons.notifications_off,
|
||||
size: 14,
|
||||
color: scheme.onSurfaceVariant,
|
||||
Expanded(
|
||||
child: Text(
|
||||
channelLabel,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (unreadCount > 0) UnreadBadge(count: unreadCount),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'CH ${channel.index}',
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: scheme.onSurfaceVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
if (lastPreview.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
lastPreview,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
if (showDragHandle && dragIndex != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
ReorderableDragStartListener(
|
||||
index: dragIndex,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// Right side: time + unread badge + muted + drag handle
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (lastTime != null)
|
||||
Text(
|
||||
_relativeTime(lastTime),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (isMuted) ...[
|
||||
Icon(
|
||||
Icons.notifications_off,
|
||||
size: 14,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
if (unreadCount > 0) UnreadBadge(count: unreadCount),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (showDragHandle && dragIndex != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
ReorderableDragStartListener(
|
||||
index: dragIndex,
|
||||
// Top-aligned with the "CH n" / time line. Bottom padding keeps
|
||||
// a comfortable drag target without pushing the icon down.
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8, right: 8, bottom: 16),
|
||||
child: Icon(
|
||||
Icons.drag_handle,
|
||||
size: 18,
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -912,11 +947,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
);
|
||||
return;
|
||||
}
|
||||
final random = Random.secure();
|
||||
final psk = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
psk[i] = random.nextInt(256);
|
||||
}
|
||||
final psk = randomBytes(16);
|
||||
Navigator.pop(sheetContext);
|
||||
await connector.setChannel(
|
||||
nextIndex,
|
||||
@@ -1545,11 +1576,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
|
||||
icon: const Icon(Icons.casino),
|
||||
tooltip: sheetContext.l10n.channels_generateRandomPsk,
|
||||
onPressed: () {
|
||||
final random = Random.secure();
|
||||
final bytes = Uint8List(16);
|
||||
for (int i = 0; i < 16; i++) {
|
||||
bytes[i] = random.nextInt(256);
|
||||
}
|
||||
final bytes = randomBytes(16);
|
||||
pskController.text = Channel.formatPskHex(bytes);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -12,6 +12,7 @@ import '../utils/platform_info.dart';
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../helpers/cyr2lat.dart';
|
||||
import '../helpers/path_helper.dart';
|
||||
import '../helpers/reaction_helper.dart';
|
||||
import '../widgets/message_status_icon.dart';
|
||||
import '../widgets/empty_state.dart';
|
||||
@@ -46,6 +47,14 @@ import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import 'telemetry_screen.dart';
|
||||
|
||||
// Image messages are deliberately absent from this screen, and there is no
|
||||
// flag to flip: the AEIC wire format is `CMD_SEND_CHANNEL_DATA` (62) /
|
||||
// GRP_DATA 0x06, which addresses a channel index rather than a contact key,
|
||||
// and the companion protocol has no direct-message equivalent (there is no
|
||||
// CMD_SEND_DATA — see `meshcore_protocol.dart`). Sending a private photo on
|
||||
// channel 0 to reach one contact would broadcast it to everyone on that
|
||||
// channel. Channel chats keep the feature; see `channel_chat_screen.dart`.
|
||||
|
||||
class ChatScreen extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final int initialUnreadCount;
|
||||
@@ -479,6 +488,9 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
languageCode: settings.translationTargetLanguageCode,
|
||||
onPressed: _showTranslationOptions,
|
||||
),
|
||||
// No image button here: the image wire format is GRP_DATA, a
|
||||
// channel primitive with no DM equivalent (see the note at the
|
||||
// top of this file).
|
||||
Expanded(
|
||||
child: ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _textController,
|
||||
@@ -546,15 +558,15 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.chat_typeMessage,
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(color: scheme.outlineVariant),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(MeshRadii.pill),
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
borderSide: BorderSide(
|
||||
color: scheme.primary,
|
||||
width: 1.5,
|
||||
@@ -776,17 +788,35 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
}
|
||||
|
||||
String _currentPathLabel(Contact contact) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
|
||||
// Check if user has set a path override
|
||||
if (contact.pathOverride != null) {
|
||||
if (contact.pathOverride! < 0) return context.l10n.chat_floodForced;
|
||||
if (contact.pathOverride == 0) return context.l10n.chat_directForced;
|
||||
return context.l10n.chat_hopsForced(contact.pathOverride!);
|
||||
final bytes = contact.pathOverrideBytes ?? Uint8List(0);
|
||||
final hopCount = _displayHopCount(
|
||||
bytes,
|
||||
contact.pathOverride!,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
return context.l10n.chat_hopsForced(hopCount);
|
||||
}
|
||||
|
||||
// Use device's path
|
||||
if (contact.pathLength < 0) return context.l10n.chat_floodAuto;
|
||||
if (contact.pathLength == 0) return context.l10n.chat_direct;
|
||||
return context.l10n.chat_hopsCount(contact.pathLength);
|
||||
final hopCount = _displayHopCount(
|
||||
contact.path,
|
||||
contact.pathLength,
|
||||
connector.pathHashByteWidth,
|
||||
);
|
||||
return context.l10n.chat_hopsCount(hopCount);
|
||||
}
|
||||
|
||||
int _displayHopCount(List<int> pathBytes, int storedHopCount, int hashWidth) {
|
||||
if (pathBytes.isEmpty) return storedHopCount;
|
||||
return PathHelper.splitPathBytes(pathBytes, hashWidth).length;
|
||||
}
|
||||
|
||||
void _showContactInfo(BuildContext context) {
|
||||
@@ -807,7 +837,10 @@ class _ChatScreenState extends State<ChatScreen> {
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.chat_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
_buildInfoRow(
|
||||
context.l10n.contact_lastSeen,
|
||||
@@ -1435,6 +1468,9 @@ class _MessageBubble extends StatelessWidget {
|
||||
color: textColor.withValues(alpha: 0.72),
|
||||
fontSize: bodyFontSize * textScale,
|
||||
),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? onLongPress
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -915,6 +915,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
return _ContactTileEntrance(
|
||||
index: index,
|
||||
contact: contact,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
lastSeen: _resolveLastSeen(contact),
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: contact.isFavorite,
|
||||
@@ -1371,7 +1372,10 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
MaterialPageRoute(
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: context.l10n.contacts_repeaterPing,
|
||||
path: Uint8List.fromList([contact.publicKey.first]),
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: _contactPathPrefix(contact, hw),
|
||||
flipPathAround: true,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1405,8 +1409,8 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
: context.l10n.contacts_roomPing,
|
||||
path: contact.pathBytesForDisplay.isNotEmpty
|
||||
? contact.pathBytesForDisplay
|
||||
: Uint8List.fromList([contact.publicKey.first]),
|
||||
flipPathAround: contact.pathBytesForDisplay.isNotEmpty,
|
||||
: _contactPathPrefix(contact, hw),
|
||||
flipPathAround: true,
|
||||
targetContact: contact,
|
||||
pathHashByteWidth: hw,
|
||||
),
|
||||
@@ -1516,6 +1520,16 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List _contactPathPrefix(Contact contact, int hashByteWidth) {
|
||||
if (contact.publicKey.isEmpty) return Uint8List(0);
|
||||
final width = hashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt()
|
||||
.clamp(1, contact.publicKey.length)
|
||||
.toInt();
|
||||
return Uint8List.fromList(contact.publicKey.sublist(0, width));
|
||||
}
|
||||
|
||||
void _confirmDelete(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
@@ -1549,6 +1563,7 @@ class _ContactsScreenState extends State<ContactsScreen>
|
||||
|
||||
class _ContactTile extends StatelessWidget {
|
||||
final Contact contact;
|
||||
final int pathHashByteWidth;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
@@ -1557,6 +1572,7 @@ class _ContactTile extends StatelessWidget {
|
||||
|
||||
const _ContactTile({
|
||||
required this.contact,
|
||||
required this.pathHashByteWidth,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
@@ -1676,7 +1692,10 @@ class _ContactTile extends StatelessWidget {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
contact.pathLabel(context.l10n),
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: pathHashByteWidth,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
@@ -1763,6 +1782,7 @@ class _ContactTile extends StatelessWidget {
|
||||
class _ContactTileEntrance extends StatelessWidget {
|
||||
final int index;
|
||||
final Contact contact;
|
||||
final int pathHashByteWidth;
|
||||
final DateTime lastSeen;
|
||||
final int unreadCount;
|
||||
final bool isFavorite;
|
||||
@@ -1772,6 +1792,7 @@ class _ContactTileEntrance extends StatelessWidget {
|
||||
const _ContactTileEntrance({
|
||||
required this.index,
|
||||
required this.contact,
|
||||
required this.pathHashByteWidth,
|
||||
required this.lastSeen,
|
||||
required this.unreadCount,
|
||||
required this.isFavorite,
|
||||
@@ -1785,6 +1806,7 @@ class _ContactTileEntrance extends StatelessWidget {
|
||||
index: index,
|
||||
child: _ContactTile(
|
||||
contact: contact,
|
||||
pathHashByteWidth: pathHashByteWidth,
|
||||
lastSeen: lastSeen,
|
||||
unreadCount: unreadCount,
|
||||
isFavorite: isFavorite,
|
||||
|
||||
@@ -147,13 +147,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
connector,
|
||||
index,
|
||||
);
|
||||
if (PlatformInfo.isDesktop) {
|
||||
return GestureDetector(
|
||||
onSecondaryTapUp: (_) =>
|
||||
_showContactContextMenu(contact, connector),
|
||||
child: tile,
|
||||
);
|
||||
}
|
||||
return tile;
|
||||
},
|
||||
),
|
||||
@@ -204,6 +197,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
}
|
||||
},
|
||||
onLongPress: () => _showContactContextMenu(contact, connector),
|
||||
onSecondaryTap: PlatformInfo.isDesktop
|
||||
? () => _showContactContextMenu(contact, connector)
|
||||
: null,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
|
||||
child: Row(
|
||||
children: [
|
||||
|
||||
@@ -20,7 +20,6 @@ import '../widgets/app_bar.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class LineOfSightEndpoint {
|
||||
final String label;
|
||||
@@ -357,7 +356,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
final settings = context.watch<AppSettingsService>().settings;
|
||||
final isImperial = settings.unitSystem == UnitSystem.imperial;
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final tileCache = context.watch<MapTileCacheService>();
|
||||
final endpoints = _visibleEndpoints();
|
||||
final mapPoints = [
|
||||
if (_start != null) _start!.point,
|
||||
@@ -430,6 +429,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
minZoom: _mapMinZoom,
|
||||
maxZoom: _mapMaxZoom,
|
||||
onLongPress: (_, point) => _addCustomPoint(point),
|
||||
onSecondaryTap: (_, point) => _addCustomPoint(point),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final shouldShow = camera.zoom >= _labelZoomThreshold;
|
||||
if (!_didReceivePositionUpdate ||
|
||||
@@ -442,8 +442,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
ThemedMapTileLayer(
|
||||
tileCache: tileCache,
|
||||
tileCache.buildTileLayer(
|
||||
context,
|
||||
opacity: _showTerrainLayer ? 1 : 0.72,
|
||||
),
|
||||
if (_result != null && _result!.segments.isNotEmpty)
|
||||
|
||||
@@ -12,7 +12,6 @@ import '../widgets/adaptive_app_bar_title.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
|
||||
class MapCacheScreen extends StatefulWidget {
|
||||
const MapCacheScreen({super.key});
|
||||
@@ -34,6 +33,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
bool _isDownloading = false;
|
||||
int _completedTiles = 0;
|
||||
int _failedTiles = 0;
|
||||
List<CachedTileInfo> _cachedTiles = const [];
|
||||
int _cachedTileBytes = 0;
|
||||
bool _isLoadingCachedTiles = false;
|
||||
double _overlayZoom = 2.0;
|
||||
LatLngBounds? _visibleBounds;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -123,8 +127,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
_minZoom = safeMin;
|
||||
_maxZoom = safeMax;
|
||||
_selectedBounds = bounds;
|
||||
_visibleBounds = bounds;
|
||||
});
|
||||
_updateEstimate();
|
||||
_refreshCachedTiles();
|
||||
if (bounds != null) {
|
||||
_mapController.fitCamera(
|
||||
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
|
||||
@@ -181,6 +187,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
|
||||
Future<void> _startDownload() async {
|
||||
final bounds = _selectedBounds;
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
if (bounds == null) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
@@ -197,6 +204,18 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!cacheService.source.allowsBulkDownload) {
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(
|
||||
context.l10n.mapCache_bulkDownloadDisabledInConfig(
|
||||
cacheService.source.label,
|
||||
),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
@@ -219,8 +238,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
|
||||
if (confirmed != true || !mounted) return;
|
||||
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
|
||||
setState(() {
|
||||
_isDownloading = true;
|
||||
_completedTiles = 0;
|
||||
@@ -254,6 +271,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
result.failed,
|
||||
)
|
||||
: context.l10n.mapCache_cachedTiles(result.downloaded);
|
||||
await _refreshCachedTiles();
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(context, content: Text(message));
|
||||
}
|
||||
|
||||
@@ -280,16 +299,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
await cacheService.clearCache();
|
||||
if (!mounted) return;
|
||||
await _refreshCachedTiles();
|
||||
if (!mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(context.l10n.mapCache_offlineCacheCleared),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _refreshCachedTiles() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoadingCachedTiles = true;
|
||||
});
|
||||
final cacheService = context.read<MapTileCacheService>();
|
||||
final inventory = await cacheService.getCachedTileInventory();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_cachedTiles = inventory.tiles;
|
||||
_cachedTileBytes = inventory.totalBytes;
|
||||
_isLoadingCachedTiles = false;
|
||||
});
|
||||
}
|
||||
|
||||
String _formatBytes(int bytes) {
|
||||
if (bytes < 1024) return '$bytes B';
|
||||
if (bytes < 1024 * 1024) {
|
||||
return '${(bytes / 1024).toStringAsFixed(1)} KB';
|
||||
}
|
||||
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final tileCache = context.watch<MapTileCacheService>();
|
||||
final source = tileCache.source;
|
||||
final selectedBounds = _selectedBounds;
|
||||
final activeCachedTiles = tileCache.filterTilesForActiveSource(
|
||||
_cachedTiles,
|
||||
);
|
||||
final cachedInSelection = tileCache.countTilesForBounds(
|
||||
activeCachedTiles,
|
||||
bounds: selectedBounds,
|
||||
minZoom: _minZoom,
|
||||
maxZoom: _maxZoom,
|
||||
);
|
||||
final overlayPolygons = tileCache.buildCachedTilePolygons(
|
||||
activeCachedTiles,
|
||||
zoom: _overlayZoom.round().clamp(0, 19).toInt(),
|
||||
visibleBounds: _visibleBounds,
|
||||
);
|
||||
final cacheSummary =
|
||||
'${context.l10n.mapCache_summarySource(source.label)}\n'
|
||||
'${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n'
|
||||
'${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n'
|
||||
'${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}';
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
@@ -327,9 +391,23 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
)
|
||||
: const KeyboardOptions.disabled(),
|
||||
),
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final nextZoom = camera.zoom;
|
||||
final nextBounds = camera.visibleBounds;
|
||||
if (_overlayZoom != nextZoom ||
|
||||
_visibleBounds?.north != nextBounds.north ||
|
||||
_visibleBounds?.south != nextBounds.south ||
|
||||
_visibleBounds?.east != nextBounds.east ||
|
||||
_visibleBounds?.west != nextBounds.west) {
|
||||
setState(() {
|
||||
_overlayZoom = nextZoom;
|
||||
_visibleBounds = nextBounds;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
tileCache.buildTileLayer(context),
|
||||
if (selectedBounds != null)
|
||||
PolygonLayer(
|
||||
polygons: [
|
||||
@@ -341,9 +419,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (overlayPolygons.isNotEmpty)
|
||||
PolygonLayer(polygons: overlayPolygons),
|
||||
],
|
||||
),
|
||||
if (isDesktop) _buildDesktopMapControls(),
|
||||
Positioned(
|
||||
top: 12,
|
||||
left: isDesktop ? 84 : 12,
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Text(
|
||||
'Z: ${_overlayZoom.round()}:',
|
||||
style: const TextStyle(fontSize: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 12,
|
||||
right: 12,
|
||||
@@ -445,6 +538,34 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
color: scheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextFormField(
|
||||
key: ValueKey(
|
||||
'$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes',
|
||||
),
|
||||
initialValue: cacheSummary,
|
||||
readOnly: true,
|
||||
minLines: 4,
|
||||
maxLines: 4,
|
||||
decoration: InputDecoration(
|
||||
labelText: _isLoadingCachedTiles
|
||||
? l10n.mapCache_cachedTilesLabel
|
||||
: l10n.mapCache_cachedTileSummaryLabel,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
if (!source.allowsBulkDownload) ...[
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.mapCache_bulkDownloadDisabledForSource(
|
||||
source.label,
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: MeshPalette.alert,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (_isDownloading) ...[
|
||||
const SizedBox(height: 8),
|
||||
LinearProgressIndicator(
|
||||
@@ -471,7 +592,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
|
||||
child: ElevatedButton.icon(
|
||||
icon: const Icon(Icons.download),
|
||||
label: Text(l10n.mapCache_downloadTilesButton),
|
||||
onPressed: _isDownloading || selectedBounds == null
|
||||
onPressed:
|
||||
_isDownloading ||
|
||||
selectedBounds == null ||
|
||||
!source.allowsBulkDownload
|
||||
? null
|
||||
: _startDownload,
|
||||
),
|
||||
|
||||
@@ -5,13 +5,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:meshcore_open/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/screens/path_trace_map.dart';
|
||||
import 'package:meshcore_open/widgets/app_bar.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../connector/meshcore_connector.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/app_settings.dart';
|
||||
import '../models/channel.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -25,7 +26,6 @@ import '../utils/battery_utils.dart';
|
||||
import '../utils/route_transitions.dart';
|
||||
import '../widgets/quick_switch_bar.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import '../widgets/themed_map_tile_layer.dart';
|
||||
import '../icons/los_icon.dart';
|
||||
import 'channels_screen.dart';
|
||||
import 'chat_screen.dart';
|
||||
@@ -79,6 +79,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
bool _hasInitializedMap = false;
|
||||
bool _removedMarkersLoaded = false;
|
||||
final List<int> _pathTrace = [];
|
||||
final List<int> _pathTraceHopWidths = [];
|
||||
final List<Contact> _pathTraceContacts = [];
|
||||
final List<LatLng> _points = [];
|
||||
final List<Polyline> _polylines = [];
|
||||
@@ -107,6 +108,31 @@ class _MapScreenState extends State<MapScreen> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
ColorScheme get _overlayScheme => Theme.of(context).colorScheme;
|
||||
|
||||
bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
Color get _overlayPanelColor => _useDarkOverlay
|
||||
? MapPalette.panelDark
|
||||
: _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96);
|
||||
|
||||
Color get _overlayPrimaryTextColor =>
|
||||
_useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface;
|
||||
|
||||
Color get _overlaySecondaryTextColor => _useDarkOverlay
|
||||
? MapPalette.textSecondary
|
||||
: _overlayScheme.onSurfaceVariant;
|
||||
|
||||
Color get _overlayMutedTextColor =>
|
||||
_useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant;
|
||||
|
||||
Color get _overlayBorderColor =>
|
||||
_useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant;
|
||||
|
||||
Color get _overlayShadowColor => _useDarkOverlay
|
||||
? MapPalette.markerShadow
|
||||
: Colors.black.withValues(alpha: 0.18);
|
||||
|
||||
_NodeAge _ageOf(Contact contact) {
|
||||
final d = DateTime.now().difference(contact.lastSeen);
|
||||
if (d.inMinutes <= 60) return _NodeAge.online;
|
||||
@@ -233,12 +259,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
bottom: 96,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 3),
|
||||
),
|
||||
@@ -250,20 +276,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.add),
|
||||
visualDensity: VisualDensity.standard,
|
||||
tooltip: context.l10n.map_zoomIn,
|
||||
onPressed: () => _zoomMapBy(1),
|
||||
),
|
||||
IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.remove),
|
||||
tooltip: context.l10n.map_zoomOut,
|
||||
onPressed: () => _zoomMapBy(-1),
|
||||
),
|
||||
IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.crop_free),
|
||||
tooltip: context.l10n.map_centerMap,
|
||||
onPressed: () => _mapController.move(center, zoom),
|
||||
@@ -285,6 +311,31 @@ class _MapScreenState extends State<MapScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _handleMapContextPress(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
LatLng latLng,
|
||||
) {
|
||||
if (_isSelectingPoi) {
|
||||
setState(() {
|
||||
_isSelectingPoi = false;
|
||||
});
|
||||
_shareMarker(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_showShareMarkerAtPositionSheet(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Builder(
|
||||
@@ -303,6 +354,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final pathHistory = context.read<PathHistoryService>();
|
||||
final tileCache = context.read<MapTileCacheService>();
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
|
||||
final allContacts = connector.allContacts;
|
||||
|
||||
@@ -376,17 +428,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Compute guessed locations with caching
|
||||
final maxRangeKm = _estimateLoRaRangeKm(connector);
|
||||
final pathHashByteWidth = connector.pathHashByteWidth
|
||||
.clamp(1, 4)
|
||||
.toInt();
|
||||
final filteredKeys = guessCandidates
|
||||
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
|
||||
.join(',');
|
||||
final anchorKeys = allContactsWithLocation
|
||||
.map(
|
||||
(c) =>
|
||||
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}',
|
||||
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${PathHelper.formatHopHex(c.path.isNotEmpty ? c.path.sublist(max(0, c.path.length - pathHashByteWidth)) : const [])}',
|
||||
)
|
||||
.join(',');
|
||||
final cacheKey =
|
||||
'$filteredKeys|$anchorKeys|$pathHistoryVersion:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
'$filteredKeys|$anchorKeys|$pathHistoryVersion:$pathHashByteWidth:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
|
||||
if (cacheKey != _guessedLocationsCacheKey) {
|
||||
_guessedLocationsCacheKey = cacheKey;
|
||||
_cachedGuessedLocations = settings.mapShowGuessedLocations
|
||||
@@ -395,6 +450,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
allContactsWithLocation,
|
||||
pathHistory,
|
||||
maxRangeKm,
|
||||
pathHashByteWidth,
|
||||
)
|
||||
: [];
|
||||
}
|
||||
@@ -554,8 +610,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
canPop: allowBack,
|
||||
child: Scaffold(
|
||||
appBar: AppBar(
|
||||
backgroundColor: MapPalette.panelDark,
|
||||
foregroundColor: MapPalette.textPrimary,
|
||||
backgroundColor: scheme.surface,
|
||||
foregroundColor: scheme.onSurface,
|
||||
title: AppBarTitle(context.l10n.map_title),
|
||||
centerTitle: true,
|
||||
automaticallyImplyLeading: false,
|
||||
@@ -708,24 +764,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
},
|
||||
onLongPress: (_, latLng) {
|
||||
if (_isSelectingPoi) {
|
||||
setState(() {
|
||||
_isSelectingPoi = false;
|
||||
});
|
||||
_shareMarker(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
defaultLabel: context.l10n.map_pointOfInterest,
|
||||
flags: 'poi',
|
||||
);
|
||||
return;
|
||||
}
|
||||
_showShareMarkerAtPositionSheet(
|
||||
context: context,
|
||||
connector: connector,
|
||||
position: latLng,
|
||||
);
|
||||
_handleMapContextPress(context, connector, latLng);
|
||||
},
|
||||
onSecondaryTap: (_, latLng) {
|
||||
_handleMapContextPress(context, connector, latLng);
|
||||
},
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
// Track zoom in half-step buckets so cluster/marker
|
||||
@@ -742,7 +784,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
},
|
||||
),
|
||||
children: [
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
tileCache.buildTileLayer(context),
|
||||
if (_polylines.isNotEmpty && _isBuildingPathTrace)
|
||||
PolylineLayer(polylines: _polylines),
|
||||
if (sharedMarkerPolylines.isNotEmpty)
|
||||
@@ -895,7 +937,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_handleQuickSwitch(index, context),
|
||||
contactsUnreadCount: connector.getTotalContactsUnreadCount(),
|
||||
channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
|
||||
highContrast: true,
|
||||
highContrast: _useDarkOverlay,
|
||||
),
|
||||
),
|
||||
floatingActionButton:
|
||||
@@ -970,23 +1012,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
List<Contact> withLocation,
|
||||
PathHistoryService pathHistory,
|
||||
double? maxRangeKm,
|
||||
int pathHashByteWidth,
|
||||
) {
|
||||
// Index known-location repeaters by their 1-byte hash.
|
||||
// null value = two repeaters share the same hash byte (ambiguous collision).
|
||||
final repeaterByHash = <int, Contact?>{};
|
||||
|
||||
for (final c in withLocation) {
|
||||
if (c.type == advTypeRepeater) {
|
||||
if (repeaterByHash.containsKey(c.publicKey[0])) {
|
||||
repeaterByHash[c.publicKey[0]] =
|
||||
null; // collision: can't disambiguate
|
||||
} else {
|
||||
repeaterByHash[c.publicKey[0]] = c;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final result = <_GuessedLocation>[];
|
||||
final hopWidth = pathHashByteWidth.clamp(1, 4).toInt();
|
||||
final anchorsByPrefix = <String, List<Contact>>{};
|
||||
for (final repeater in withLocation) {
|
||||
if (repeater.type != advTypeRepeater) continue;
|
||||
if (repeater.publicKey.length < hopWidth) continue;
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
repeater.publicKey.sublist(0, hopWidth),
|
||||
);
|
||||
anchorsByPrefix.putIfAbsent(prefix, () => []).add(repeater);
|
||||
}
|
||||
|
||||
for (final contact in allContacts) {
|
||||
if (contact.hasLocation) continue;
|
||||
@@ -1000,21 +1038,23 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Collect the contact-side (last-hop) repeater from every known path.
|
||||
// path = [device-side hop, ..., contact-side hop]
|
||||
// Only path.last is actually within radio range of the contact — using
|
||||
// earlier bytes would anchor against our own side of the network.
|
||||
// Only the last hop chunk is actually within radio range of the contact.
|
||||
final pathSets = <List<int>>[
|
||||
contact.path.toList(),
|
||||
...pathHistory
|
||||
.getRecentPaths(contact.publicKeyHex)
|
||||
.map((r) => r.pathBytes),
|
||||
];
|
||||
final lastHopBytes = <int>{};
|
||||
for (final pathBytes in pathSets) {
|
||||
if (pathBytes.isEmpty) continue;
|
||||
final lastHop = pathBytes.last;
|
||||
lastHopBytes.add(lastHop);
|
||||
final r = repeaterByHash[lastHop];
|
||||
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!));
|
||||
final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth));
|
||||
if (lastHop.isEmpty) continue;
|
||||
|
||||
final repeaters = anchorsByPrefix[PathHelper.formatHopHex(lastHop)];
|
||||
if (repeaters != null && repeaters.isNotEmpty) {
|
||||
final repeater = repeaters.first;
|
||||
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
|
||||
}
|
||||
}
|
||||
|
||||
// Filter anchors that are geometrically inconsistent with radio range.
|
||||
@@ -1181,9 +1221,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onLongPress: () => _isBuildingPathTrace
|
||||
? _showNodeInfo(context, guess.contact)
|
||||
: null,
|
||||
onLongPress: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
|
||||
},
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, guess.contact, position: guess.position)
|
||||
: _selectNode(guess.contact, guessedPosition: guess.position),
|
||||
@@ -1193,14 +1236,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
height: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
border: Border.all(
|
||||
color: guess.highConfidence ? color : MapPalette.textMuted,
|
||||
color: guess.highConfidence ? color : _overlayMutedTextColor,
|
||||
width: guess.highConfidence ? 2.5 : 2,
|
||||
),
|
||||
boxShadow: const [
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 7,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
@@ -1209,7 +1252,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
Icons.not_listed_location,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
size: 19,
|
||||
),
|
||||
),
|
||||
@@ -1286,12 +1329,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
// Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms
|
||||
// whose first key byte collides with another repeater/room on the map.
|
||||
final overlapPrefixes = <int>{};
|
||||
final overlapPrefixes = <String>{};
|
||||
if (overlapsMode) {
|
||||
final counts = <int, int>{};
|
||||
final hopWidth = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt();
|
||||
final counts = <String, int>{};
|
||||
for (final contact in contacts) {
|
||||
if (contact.type == advTypeRepeater || contact.type == advTypeRoom) {
|
||||
final prefix = contact.publicKey.first;
|
||||
if ((contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
contact.publicKey.length >= hopWidth) {
|
||||
final prefix = PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, hopWidth),
|
||||
);
|
||||
counts[prefix] = (counts[prefix] ?? 0) + 1;
|
||||
}
|
||||
}
|
||||
@@ -1299,10 +1350,20 @@ class _MapScreenState extends State<MapScreen> {
|
||||
if (count > 1) overlapPrefixes.add(prefix);
|
||||
});
|
||||
}
|
||||
final overlapHopWidth = context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth
|
||||
.clamp(1, pubKeySize)
|
||||
.toInt();
|
||||
bool isOverlap(Contact contact) =>
|
||||
overlapsMode &&
|
||||
(contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
|
||||
overlapPrefixes.contains(contact.publicKey.first);
|
||||
contact.publicKey.length >= overlapHopWidth &&
|
||||
overlapPrefixes.contains(
|
||||
PathHelper.formatHopHex(
|
||||
contact.publicKey.sublist(0, overlapHopWidth),
|
||||
),
|
||||
);
|
||||
|
||||
void addNode(Contact contact, {bool dot = false}) {
|
||||
final overlap = isOverlap(contact);
|
||||
@@ -1383,8 +1444,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
height: size,
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onLongPress: () =>
|
||||
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null,
|
||||
onLongPress: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||
},
|
||||
onSecondaryTap: () {
|
||||
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
|
||||
},
|
||||
onTap: () => _isBuildingPathTrace
|
||||
? _addToPath(context, contact)
|
||||
: _selectNode(contact),
|
||||
@@ -1523,12 +1588,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.xs),
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 4,
|
||||
offset: Offset(0, 1),
|
||||
),
|
||||
@@ -1542,7 +1607,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1663,7 +1728,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: statusColor,
|
||||
border: Border.all(color: MapPalette.panelDark, width: 2),
|
||||
border: Border.all(color: _overlayPanelColor, width: 2),
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: batteryLow
|
||||
@@ -1686,10 +1751,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: MapPalette.textSecondary,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -1747,9 +1812,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Expanded(
|
||||
child: Material(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
shape: StadiumBorder(
|
||||
side: const BorderSide(color: MapPalette.border),
|
||||
side: BorderSide(color: _overlayBorderColor),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: TextField(
|
||||
@@ -1757,17 +1822,15 @@ class _MapScreenState extends State<MapScreen> {
|
||||
focusNode: _searchFocus,
|
||||
decoration: InputDecoration(
|
||||
hintText: context.l10n.map_searchHint,
|
||||
hintStyle: const TextStyle(
|
||||
color: MapPalette.textSecondary,
|
||||
),
|
||||
prefixIcon: const Icon(
|
||||
hintStyle: TextStyle(color: _overlaySecondaryTextColor),
|
||||
prefixIcon: Icon(
|
||||
Icons.search,
|
||||
size: 20,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
suffixIcon: hasQuery
|
||||
? IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.close, size: 18),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
@@ -1787,9 +1850,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
cursorColor: MapPalette.selected,
|
||||
@@ -1801,9 +1864,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Material(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
shape: StadiumBorder(
|
||||
side: const BorderSide(color: MapPalette.border),
|
||||
side: BorderSide(color: _overlayBorderColor),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
@@ -1827,17 +1890,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
AnimatedRotation(
|
||||
turns: _statsExpanded ? 0.5 : 0,
|
||||
duration: const Duration(milliseconds: 200),
|
||||
child: const Icon(
|
||||
child: Icon(
|
||||
Icons.expand_more,
|
||||
size: 16,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -1936,12 +1999,12 @@ class _MapScreenState extends State<MapScreen> {
|
||||
color: selected
|
||||
? Color.alphaBlend(
|
||||
accent.withValues(alpha: 0.34),
|
||||
MapPalette.panelDark,
|
||||
_overlayPanelColor,
|
||||
)
|
||||
: MapPalette.panelDark,
|
||||
: _overlayPanelColor,
|
||||
shape: StadiumBorder(
|
||||
side: BorderSide(
|
||||
color: selected ? accent : MapPalette.border,
|
||||
color: selected ? accent : _overlayBorderColor,
|
||||
width: selected ? 1.5 : 1,
|
||||
),
|
||||
),
|
||||
@@ -1957,11 +2020,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (selected) ...[
|
||||
const Icon(
|
||||
Icons.check,
|
||||
size: 13,
|
||||
color: MapPalette.textPrimary,
|
||||
),
|
||||
Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor),
|
||||
const SizedBox(width: 4),
|
||||
],
|
||||
Text(
|
||||
@@ -1970,8 +2029,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
fontSize: 12.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: selected
|
||||
? MapPalette.textPrimary
|
||||
: MapPalette.textSecondary,
|
||||
? _overlayPrimaryTextColor
|
||||
: _overlaySecondaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2001,14 +2060,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
margin: const EdgeInsets.only(top: 6),
|
||||
constraints: const BoxConstraints(maxHeight: 300),
|
||||
decoration: BoxDecoration(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2017,8 +2076,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(
|
||||
context.l10n.map_noResults,
|
||||
style: const TextStyle(
|
||||
color: MapPalette.textSecondary,
|
||||
style: TextStyle(
|
||||
color: _overlaySecondaryTextColor,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
@@ -2028,7 +2087,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
padding: const EdgeInsets.symmetric(vertical: 4),
|
||||
itemCount: results.length,
|
||||
separatorBuilder: (_, _) =>
|
||||
const Divider(height: 1, color: MapPalette.border),
|
||||
Divider(height: 1, color: _overlayBorderColor),
|
||||
itemBuilder: (context, index) {
|
||||
final c = results[index];
|
||||
final color = _getNodeColor(c.type);
|
||||
@@ -2049,10 +2108,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Text(
|
||||
c.name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 13.5,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2060,7 +2119,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
c.publicKeyHex.substring(0, 12),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10.5,
|
||||
color: MapPalette.textSecondary,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2070,13 +2129,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Icon(
|
||||
Icons.chevron_right,
|
||||
size: 18,
|
||||
color: MapPalette.textSecondary,
|
||||
color: _overlaySecondaryTextColor,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
context.l10n.map_noGps.toUpperCase(),
|
||||
style: MeshTheme.accentLabel(
|
||||
color: MapPalette.textMuted,
|
||||
color: _overlayMutedTextColor,
|
||||
fontSize: 8.5,
|
||||
),
|
||||
),
|
||||
@@ -2136,14 +2195,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
width: 230,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
|
||||
decoration: BoxDecoration(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2160,7 +2219,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
_statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline),
|
||||
_statRow(context.l10n.map_markers, pinCount, MapPalette.shared),
|
||||
const Divider(height: 16, color: MapPalette.border),
|
||||
Divider(height: 16, color: _overlayBorderColor),
|
||||
_buildLegendItem(
|
||||
Icons.person,
|
||||
context.l10n.map_chat,
|
||||
@@ -2190,7 +2249,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
_buildLegendItem(
|
||||
Icons.not_listed_location,
|
||||
context.l10n.map_guessedLocation,
|
||||
MapPalette.textMuted,
|
||||
_overlayMutedTextColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -2211,7 +2270,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Expanded(
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary),
|
||||
style: TextStyle(
|
||||
fontSize: 12.5,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
@@ -2220,7 +2282,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -2253,8 +2315,8 @@ class _MapScreenState extends State<MapScreen> {
|
||||
child: MeshCard(
|
||||
margin: EdgeInsets.zero,
|
||||
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
|
||||
color: MapPalette.panelDark,
|
||||
borderColor: MapPalette.border,
|
||||
color: _overlayPanelColor,
|
||||
borderColor: _overlayBorderColor,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
@@ -2277,10 +2339,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
contact.name,
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2308,9 +2370,9 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Flexible(
|
||||
child: Text(
|
||||
contact.typeLabel(context.l10n),
|
||||
style: const TextStyle(
|
||||
style: TextStyle(
|
||||
fontSize: 11.5,
|
||||
color: MapPalette.textSecondary,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
@@ -2322,13 +2384,13 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
if (pos != null)
|
||||
IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.center_focus_strong, size: 20),
|
||||
tooltip: context.l10n.map_centerOnNode,
|
||||
onPressed: () => _mapController.move(pos, max(_zoom, 15)),
|
||||
),
|
||||
IconButton(
|
||||
color: MapPalette.textPrimary,
|
||||
color: _overlayPrimaryTextColor,
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
onPressed: _clearSelection,
|
||||
),
|
||||
@@ -2345,7 +2407,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
),
|
||||
_miniMeta(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
_miniMeta('ID', contact.publicKeyHex.substring(0, 12)),
|
||||
if (pos != null)
|
||||
@@ -2389,14 +2454,17 @@ class _MapScreenState extends State<MapScreen> {
|
||||
Text(
|
||||
label.toUpperCase(),
|
||||
style: MeshTheme.accentLabel(
|
||||
color: MapPalette.textMuted,
|
||||
color: _overlayMutedTextColor,
|
||||
fontSize: 8,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 1),
|
||||
Text(
|
||||
value,
|
||||
style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 11.5,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -2746,7 +2814,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
_buildInfoRow(
|
||||
context.l10n.map_path,
|
||||
contact.pathLabel(context.l10n),
|
||||
contact.pathLabel(
|
||||
context.l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
),
|
||||
if (contact.hasLocation)
|
||||
_buildInfoRow(
|
||||
@@ -3492,10 +3563,23 @@ class _MapScreenState extends State<MapScreen> {
|
||||
}
|
||||
|
||||
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final hopWidth = min(
|
||||
connector.pathHashByteWidth.clamp(1, pubKeySize),
|
||||
contact.publicKey.length,
|
||||
).toInt();
|
||||
final hopPrefix = contact.publicKey.sublist(0, hopWidth);
|
||||
for (final existingHop in PathHelper.splitPathBytes(
|
||||
_pathTrace,
|
||||
connector.pathHashByteWidth,
|
||||
)) {
|
||||
if (listEquals(existingHop, hopPrefix)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
setState(() {
|
||||
_pathTrace.add(
|
||||
contact.publicKey[0],
|
||||
); // Add first 16 bytes of public key to path trace
|
||||
_pathTrace.addAll(hopPrefix); // Add the hop-width pubkey prefix.
|
||||
_pathTraceHopWidths.add(hopWidth);
|
||||
_pathTraceContacts.add(
|
||||
contact.copyWith(
|
||||
latitude: position?.latitude ?? contact.latitude,
|
||||
@@ -3510,6 +3594,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = true;
|
||||
_pathTrace.clear();
|
||||
_pathTraceHopWidths.clear();
|
||||
_pathTraceContacts.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
@@ -3519,8 +3604,19 @@ class _MapScreenState extends State<MapScreen> {
|
||||
|
||||
void _removePath() {
|
||||
setState(() {
|
||||
final recordedHopWidth = _pathTraceHopWidths.isNotEmpty
|
||||
? _pathTraceHopWidths.removeLast()
|
||||
: context.read<MeshCoreConnector>().pathHashByteWidth.clamp(
|
||||
1,
|
||||
pubKeySize,
|
||||
);
|
||||
final hopByteCount = min(recordedHopWidth, _pathTrace.length).toInt();
|
||||
_pathTraceContacts.removeLast();
|
||||
_pathTrace.removeLast(); // Remove last node from path trace
|
||||
// A path trace hop can be wider than one byte; remove the full hash prefix.
|
||||
_pathTrace.removeRange(
|
||||
_pathTrace.length - hopByteCount,
|
||||
_pathTrace.length,
|
||||
);
|
||||
_points.removeLast(); // Remove last point from points list
|
||||
_polylines.clear(); // Clear polylines
|
||||
});
|
||||
@@ -3537,14 +3633,14 @@ class _MapScreenState extends State<MapScreen> {
|
||||
right: 16,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: MapPalette.panelDark,
|
||||
color: _overlayPanelColor,
|
||||
borderRadius: BorderRadius.circular(MeshRadii.md),
|
||||
border: Border.all(color: MapPalette.border),
|
||||
boxShadow: const [
|
||||
border: Border.all(color: _overlayBorderColor),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: MapPalette.markerShadow,
|
||||
color: _overlayShadowColor,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 4),
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -3557,24 +3653,34 @@ class _MapScreenState extends State<MapScreen> {
|
||||
children: [
|
||||
Text(
|
||||
l10n.contacts_pathTrace,
|
||||
style: TextStyle(fontWeight: FontWeight.bold),
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: _overlayPrimaryTextColor,
|
||||
),
|
||||
),
|
||||
if (_pathTrace.isEmpty) const SizedBox(height: 8),
|
||||
if (_pathTrace.isEmpty)
|
||||
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)),
|
||||
Text(
|
||||
l10n.map_tapToAdd,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (_pathTrace.isNotEmpty)
|
||||
Text(
|
||||
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: MapPalette.textSecondary,
|
||||
color: _overlaySecondaryTextColor,
|
||||
),
|
||||
),
|
||||
SelectableText(
|
||||
_pathTrace
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||
.join(','),
|
||||
PathHelper.splitPathBytes(
|
||||
_pathTrace,
|
||||
context.read<MeshCoreConnector>().pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(','),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w700,
|
||||
@@ -3599,6 +3705,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
builder: (context) => PathTraceMapScreen(
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
flipPathAround: false,
|
||||
pathHashByteWidth: hashW,
|
||||
pathContacts: _pathTraceContacts,
|
||||
),
|
||||
@@ -3621,6 +3728,10 @@ class _MapScreenState extends State<MapScreen> {
|
||||
title: l10n.contacts_pathTrace,
|
||||
path: Uint8List.fromList(_pathTrace),
|
||||
flipPathAround: true,
|
||||
pathHashByteWidth: context
|
||||
.read<MeshCoreConnector>()
|
||||
.pathHashByteWidth,
|
||||
pathContacts: _pathTraceContacts,
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -3643,6 +3754,7 @@ class _MapScreenState extends State<MapScreen> {
|
||||
setState(() {
|
||||
_isBuildingPathTrace = false;
|
||||
_pathTrace.clear();
|
||||
_pathTraceHopWidths.clear();
|
||||
_points.clear();
|
||||
_polylines.clear();
|
||||
});
|
||||
|
||||
@@ -276,7 +276,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
@@ -296,7 +296,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
|
||||
),
|
||||
],
|
||||
),
|
||||
centerTitle: false,
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(isFloodMode ? Icons.waves : Icons.route),
|
||||
|
||||
@@ -7,6 +7,7 @@ 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/helpers/path_helper.dart';
|
||||
import 'package:meshcore_open/l10n/l10n.dart';
|
||||
import 'package:meshcore_open/models/app_settings.dart';
|
||||
import 'package:meshcore_open/models/contact.dart';
|
||||
@@ -19,7 +20,6 @@ import 'package:meshcore_open/services/path_history_service.dart';
|
||||
import 'package:meshcore_open/utils/app_logger.dart';
|
||||
import 'package:meshcore_open/widgets/path_map_ui.dart';
|
||||
import 'package:meshcore_open/widgets/snr_indicator.dart';
|
||||
import 'package:meshcore_open/widgets/themed_map_tile_layer.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
|
||||
@@ -27,9 +27,9 @@ export 'package:meshcore_open/widgets/path_map_ui.dart'
|
||||
show formatDistance, getPathDistanceMeters;
|
||||
|
||||
class PathTraceData {
|
||||
final Uint8List pathData;
|
||||
final List<Uint8List> pathData;
|
||||
final List<double> snrData;
|
||||
final Map<int, Contact> pathContacts;
|
||||
final Map<String, Contact> pathContacts;
|
||||
|
||||
PathTraceData({
|
||||
required this.pathData,
|
||||
@@ -38,6 +38,20 @@ class PathTraceData {
|
||||
});
|
||||
}
|
||||
|
||||
String _hopKey(Uint8List hopBytes) => PathHelper.formatHopHex(hopBytes);
|
||||
|
||||
Uint8List _lastHopChunk(Uint8List path, int hopWidth) {
|
||||
if (path.isEmpty) return Uint8List(0);
|
||||
final width = hopWidth.clamp(1, path.length).toInt();
|
||||
return Uint8List.fromList(path.sublist(path.length - width));
|
||||
}
|
||||
|
||||
bool _matchesHopPrefix(Uint8List a, Uint8List b) {
|
||||
if (a.isEmpty || b.isEmpty) return false;
|
||||
final width = min(a.length, b.length);
|
||||
return listEquals(a.sublist(0, width), b.sublist(0, width));
|
||||
}
|
||||
|
||||
class PathTraceMapScreen extends StatefulWidget {
|
||||
final String title;
|
||||
final Uint8List path;
|
||||
@@ -80,8 +94,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
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 = {};
|
||||
// Inferred positions for hops that have no GPS location, keyed by hop prefix.
|
||||
Map<String, LatLng> _inferredHopPositions = {};
|
||||
// Endpoint position for the target contact (GPS or guessed).
|
||||
LatLng? _targetContactPosition;
|
||||
bool _targetContactIsGuessed = false;
|
||||
@@ -94,6 +108,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
double _pathDistanceMeters = 0.0;
|
||||
bool _showNodeLabels = true;
|
||||
Contact? _targetContact;
|
||||
Uint8List? _sentTagBytes;
|
||||
// Live path resolved at trace time; used by the response handler for
|
||||
// endpoint inference so it matches the path that was actually traced.
|
||||
Uint8List _tracedPath = Uint8List(0);
|
||||
@@ -103,17 +118,82 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
PathHistoryService? _pathHistory;
|
||||
PathViewMode _viewMode = PathViewMode.single;
|
||||
List<DisplayPath> _displayPaths = [];
|
||||
List<int> _primaryOutboundHops = [];
|
||||
List<Uint8List> _primaryOutboundHops = [];
|
||||
String _selectedPathId = 'primary';
|
||||
final Set<String> _hiddenPathIds = {};
|
||||
bool _panelCollapsed = false;
|
||||
bool _animationEnabled = true;
|
||||
bool _followPacket = false;
|
||||
|
||||
String _formatPathPrefixes(Uint8List pathBytes) {
|
||||
return pathBytes
|
||||
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||
.join(',');
|
||||
String _formatPathPrefixes(Uint8List pathBytes, [int? hashByteWidth]) {
|
||||
return PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hashByteWidth ?? widget.pathHashByteWidth,
|
||||
).map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
int _traceHashByteWidth(int pathHashByteWidth) {
|
||||
final width = pathHashByteWidth.clamp(1, pubKeySize).toInt();
|
||||
if (width <= 1) return 1;
|
||||
if (width == 2) return 2;
|
||||
// Trace packets encode hash width as 1 << flags, so 3-byte path hashes
|
||||
// must be traced with a 4-byte public-key prefix.
|
||||
return 4;
|
||||
}
|
||||
|
||||
int _traceFlagsForHashWidth(int traceHashByteWidth) {
|
||||
if (traceHashByteWidth <= 1) return 0;
|
||||
if (traceHashByteWidth == 2) return 1;
|
||||
return 2;
|
||||
}
|
||||
|
||||
Uint8List _reversePathByHop(Uint8List pathBytes, int hopWidth) {
|
||||
final reversedHops = PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
hopWidth,
|
||||
).reversed;
|
||||
final bytes = <int>[];
|
||||
for (final hop in reversedHops) {
|
||||
bytes.addAll(hop);
|
||||
}
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
Uint8List? _expandHopForTrace(
|
||||
Uint8List hop,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
if (hop.length == traceHashByteWidth) return hop;
|
||||
|
||||
final candidates = <Contact>[
|
||||
...?widget.pathContacts,
|
||||
if (widget.targetContact != null) widget.targetContact!,
|
||||
...connector.allContactsUnfiltered,
|
||||
];
|
||||
for (final contact in candidates) {
|
||||
if (contact.publicKey.length < traceHashByteWidth) continue;
|
||||
if (!listEquals(contact.publicKey.sublist(0, hop.length), hop)) continue;
|
||||
return Uint8List.fromList(
|
||||
contact.publicKey.sublist(0, traceHashByteWidth),
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Uint8List? _tracePathFromBytes(
|
||||
Uint8List pathBytes,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final hops = PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth);
|
||||
final traceBytes = <int>[];
|
||||
for (final hop in hops) {
|
||||
final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
|
||||
if (traceHop == null) return null;
|
||||
traceBytes.addAll(traceHop);
|
||||
}
|
||||
return Uint8List.fromList(traceBytes);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -233,45 +313,62 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Uint8List buildPath(Uint8List pathBytes) {
|
||||
Uint8List traceBytes;
|
||||
Uint8List? buildPath(
|
||||
Uint8List pathBytes,
|
||||
int traceHashByteWidth,
|
||||
MeshCoreConnector connector,
|
||||
) {
|
||||
final pathHops = PathHelper.splitPathBytes(
|
||||
pathBytes,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
final hopWidth = traceHashByteWidth.clamp(1, pubKeySize).toInt();
|
||||
|
||||
if (pathBytes.isEmpty) {
|
||||
final pk = widget.targetContact?.publicKey;
|
||||
final n = widget.pathHashByteWidth.clamp(1, pubKeySize);
|
||||
if (pk != null && pk.length >= n) {
|
||||
return Uint8List.fromList(pk.sublist(0, n));
|
||||
// Compute targetPrefix if targetContact is provided
|
||||
Uint8List? targetPrefix;
|
||||
if (widget.targetContact != null) {
|
||||
final pk = widget.targetContact!.publicKey;
|
||||
if (pk.isNotEmpty) {
|
||||
final len = pk.length >= hopWidth ? hopWidth : pk.length;
|
||||
targetPrefix = Uint8List.fromList(pk.sublist(0, len));
|
||||
}
|
||||
traceBytes = Uint8List(1);
|
||||
traceBytes[0] = pk?[0] ?? 0;
|
||||
return traceBytes;
|
||||
}
|
||||
|
||||
if (widget.targetContact?.type == advTypeRepeater ||
|
||||
widget.targetContact?.type == advTypeRoom) {
|
||||
final len = (pathBytes.length + pathBytes.length + 1);
|
||||
traceBytes = Uint8List(len);
|
||||
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0;
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
final outboundHops = <Uint8List>[];
|
||||
for (final hop in pathHops) {
|
||||
final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
|
||||
if (traceHop == null) return null;
|
||||
outboundHops.add(traceHop);
|
||||
}
|
||||
if (targetPrefix != null) {
|
||||
// Check if targetPrefix is already the last hop in pathHops to avoid duplication
|
||||
bool alreadyEndedWithTarget = false;
|
||||
if (outboundHops.isNotEmpty) {
|
||||
if (listEquals(outboundHops.last, targetPrefix)) {
|
||||
alreadyEndedWithTarget = true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (pathBytes.length < 2) {
|
||||
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
|
||||
}
|
||||
final len = (pathBytes.length + pathBytes.length - 1);
|
||||
traceBytes = Uint8List(len);
|
||||
for (int i = 0; i < pathBytes.length; i++) {
|
||||
traceBytes[i] = pathBytes[i];
|
||||
if (i < pathBytes.length - 1) {
|
||||
traceBytes[len - 1 - i] = pathBytes[i];
|
||||
}
|
||||
if (!alreadyEndedWithTarget) {
|
||||
outboundHops.add(targetPrefix);
|
||||
}
|
||||
}
|
||||
return traceBytes;
|
||||
|
||||
if (outboundHops.isEmpty) {
|
||||
return Uint8List(0);
|
||||
}
|
||||
|
||||
final mirroredHops = <Uint8List>[...outboundHops];
|
||||
if (outboundHops.length > 1) {
|
||||
mirroredHops.addAll(
|
||||
outboundHops.sublist(0, outboundHops.length - 1).reversed,
|
||||
);
|
||||
}
|
||||
|
||||
final traceBytes = <int>[];
|
||||
for (final hop in mirroredHops) {
|
||||
traceBytes.addAll(hop);
|
||||
}
|
||||
return Uint8List.fromList(traceBytes);
|
||||
}
|
||||
|
||||
/// Resolves the path bytes to trace. When tracing a specific contact's
|
||||
@@ -301,26 +398,49 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
final connector = Provider.of<MeshCoreConnector>(context, listen: false);
|
||||
final traceHashByteWidth = _traceHashByteWidth(widget.pathHashByteWidth);
|
||||
final livePath = _resolveLivePath(connector);
|
||||
_tracedPath = livePath;
|
||||
|
||||
final pathTmp = widget.reversePathAround
|
||||
? Uint8List.fromList(livePath.reversed.toList())
|
||||
? _reversePathByHop(livePath, widget.pathHashByteWidth)
|
||||
: livePath;
|
||||
|
||||
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp;
|
||||
final path = widget.flipPathAround
|
||||
? buildPath(pathTmp, traceHashByteWidth, connector)
|
||||
: _tracePathFromBytes(pathTmp, traceHashByteWidth, connector);
|
||||
|
||||
if (path == null) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isLoading = false;
|
||||
_failed2Loaded = true;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
appLogger.info(
|
||||
'Initiating path trace with path: ${_formatPathPrefixes(path)}',
|
||||
'Initiating path trace with path: '
|
||||
'${_formatPathPrefixes(path, traceHashByteWidth)}',
|
||||
tag: 'PathTraceMapScreen',
|
||||
noNotify: !mounted,
|
||||
);
|
||||
|
||||
final sentTag = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
_sentTagBytes = Uint8List(4)
|
||||
..[0] = sentTag & 0xFF
|
||||
..[1] = (sentTag >> 8) & 0xFF
|
||||
..[2] = (sentTag >> 16) & 0xFF
|
||||
..[3] = (sentTag >> 24) & 0xFF;
|
||||
|
||||
final flags = _traceFlagsForHashWidth(traceHashByteWidth);
|
||||
final tracePayload = path.isEmpty ? Uint8List.fromList([0x00]) : path;
|
||||
|
||||
final frame = buildTraceReq(
|
||||
DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
0, //auth
|
||||
0, //flag
|
||||
payload: path,
|
||||
sentTag,
|
||||
0, // auth
|
||||
flags, // flag
|
||||
payload: tracePayload,
|
||||
);
|
||||
connector.sendFrame(frame);
|
||||
}
|
||||
@@ -364,15 +484,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
// Check if it's a binary response
|
||||
if (frame.length > 8 &&
|
||||
if (frame.length >= 12 &&
|
||||
code == pushCodeTraceData &&
|
||||
listEquals(frame.sublist(4, 8), tagData)) {
|
||||
(listEquals(frame.sublist(4, 8), _sentTagBytes) ||
|
||||
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);
|
||||
}
|
||||
_handleTraceResponse(frame);
|
||||
}
|
||||
} catch (e) {
|
||||
_timeoutTimer?.cancel();
|
||||
@@ -393,21 +511,30 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final buffer = BufferReader(frame);
|
||||
try {
|
||||
buffer.skipBytes(2); // Skip push code and reserved byte
|
||||
int pathLength = buffer.readUInt8();
|
||||
final int flags = buffer
|
||||
.readUInt8(); // path_sz = flags & 0x03 (path-hash mode, fw v1.11+)
|
||||
final pathLenByte = buffer.readUInt8();
|
||||
final flags = buffer.readUInt8();
|
||||
buffer.skipBytes(4); // Skip tag data
|
||||
buffer.skipBytes(4); // Skip auth code
|
||||
final int pathSz = flags & 0x03;
|
||||
Uint8List pathData = buffer.readBytes(pathLength);
|
||||
var width = _traceHashByteWidth(1 << (flags & 0x03));
|
||||
var pathLength = pathLenByte == 0xFF ? 0 : pathLenByte;
|
||||
if (pathLength > buffer.remaining && (pathLenByte & 0xC0) != 0) {
|
||||
final packedWidth = ((pathLenByte & 0xC0) >> 6) + 1;
|
||||
final packedLength = (pathLenByte & 0x3F) * packedWidth;
|
||||
if (packedLength <= buffer.remaining) {
|
||||
width = packedWidth;
|
||||
pathLength = packedLength;
|
||||
}
|
||||
}
|
||||
final pathBytes = buffer.readBytes(pathLength);
|
||||
final pathData = PathHelper.splitPathBytes(pathBytes, width);
|
||||
// Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node).
|
||||
final int snrCount = (pathLength >> pathSz) + 1;
|
||||
final snrCount = (pathLength ~/ width) + 1;
|
||||
List<double> snrData = buffer
|
||||
.readBytes(snrCount)
|
||||
.map((snr) => snr.toSigned(8).toDouble() / 4)
|
||||
.toList();
|
||||
|
||||
Map<int, Contact> pathContacts = {};
|
||||
Map<String, Contact> pathContacts = {};
|
||||
Contact lastContact = Contact(
|
||||
path: Uint8List(0),
|
||||
pathLength: 0,
|
||||
@@ -419,7 +546,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
lastSeen: DateTime.now(),
|
||||
);
|
||||
if (widget.pathContacts != null) {
|
||||
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c};
|
||||
final hopWidth = width.clamp(1, pubKeySize).toInt();
|
||||
pathContacts = {
|
||||
for (var c in widget.pathContacts!)
|
||||
if (c.publicKey.length >= hopWidth)
|
||||
_hopKey(Uint8List.fromList(c.publicKey.sublist(0, hopWidth))): c,
|
||||
};
|
||||
} else {
|
||||
final contacts = connector.allContactsUnfiltered;
|
||||
contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
|
||||
@@ -434,12 +566,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
_maxRepeaterMatchDistanceMeters) {
|
||||
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
|
||||
}
|
||||
for (var repeaterData in pathData) {
|
||||
for (final repeaterData in pathData) {
|
||||
final hopWidth = repeaterData.length;
|
||||
if (repeater.publicKey.length < hopWidth) continue;
|
||||
if (listEquals(
|
||||
repeater.publicKey.sublist(0, 1),
|
||||
Uint8List.fromList([repeaterData]),
|
||||
repeater.publicKey.sublist(0, hopWidth),
|
||||
repeaterData,
|
||||
)) {
|
||||
pathContacts[repeaterData] = repeater;
|
||||
pathContacts[_hopKey(repeaterData)] = repeater;
|
||||
lastContact = repeater;
|
||||
}
|
||||
}
|
||||
@@ -448,13 +582,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
// For hops with no GPS contact, infer position from other contacts
|
||||
// with known GPS that share the same last-hop byte.
|
||||
final Map<int, LatLng> inferredPositions = {};
|
||||
final Map<String, LatLng> inferredPositions = {};
|
||||
for (final hop in pathData) {
|
||||
final contact = pathContacts[hop];
|
||||
final hopKey = _hopKey(hop);
|
||||
final contact = pathContacts[hopKey];
|
||||
if (contact != null && contact.hasLocation) continue;
|
||||
final peers = connector.contacts
|
||||
.where(
|
||||
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop,
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
hop,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
@@ -464,7 +605,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
|
||||
peers.length;
|
||||
inferredPositions[hop] = LatLng(lat, lon);
|
||||
inferredPositions[hopKey] = LatLng(lat, lon);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,20 +627,31 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final tc = _targetContact!;
|
||||
if (tc.hasLocation) {
|
||||
targetPos = LatLng(tc.latitude!, tc.longitude!);
|
||||
} else if (_tracedPath.length > 1) {
|
||||
} else if (pathData.length > 1 || _tracedPath.isNotEmpty) {
|
||||
// Infer from the last hop: average GPS contacts sharing that hop.
|
||||
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop
|
||||
// sits in the middle of the symmetric sequence; .last is the local side.
|
||||
final tracedHops = PathHelper.splitPathBytes(
|
||||
_tracedPath,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
final hopsForEndpoint = tracedHops.isNotEmpty
|
||||
? tracedHops
|
||||
: pathData;
|
||||
final lastHop = widget.reversePathAround
|
||||
? _tracedPath.first
|
||||
: _tracedPath.last;
|
||||
? hopsForEndpoint.first
|
||||
: hopsForEndpoint.last;
|
||||
final lastHopKey = _hopKey(lastHop);
|
||||
|
||||
final peers = connector.allContacts
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
c.path.last == lastHop,
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
lastHop,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
if (peers.isNotEmpty) {
|
||||
@@ -516,9 +668,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
lon + offsetDeg * sin(angle),
|
||||
);
|
||||
targetGuessed = true;
|
||||
} else if (inferredPositions.containsKey(lastHop)) {
|
||||
final lat = inferredPositions[lastHop]!.latitude;
|
||||
final lon = inferredPositions[lastHop]!.longitude;
|
||||
} else if (inferredPositions.containsKey(lastHopKey)) {
|
||||
final lat = inferredPositions[lastHopKey]!.latitude;
|
||||
final lon = inferredPositions[lastHopKey]!.longitude;
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
targetPos = LatLng(
|
||||
@@ -528,7 +680,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
targetGuessed = true;
|
||||
} else {
|
||||
// As a last resort, just place it at the same position as the last hop.
|
||||
final contact = pathContacts[lastHop];
|
||||
final contact = pathContacts[lastHopKey];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
const offsetDeg = 0.003;
|
||||
final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
|
||||
@@ -546,21 +698,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
_points = <LatLng>[];
|
||||
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
String hopLast = '';
|
||||
String hopLastLast = '';
|
||||
for (final hop in _traceData!.pathData) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
final hopKey = _hopKey(hop);
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) {
|
||||
break; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
final contact = _traceData!.pathContacts[hopKey];
|
||||
if (contact != null && contact.hasLocation) {
|
||||
_points.add(LatLng(contact.latitude!, contact.longitude!));
|
||||
} else {
|
||||
final inferred = inferredPositions[hop];
|
||||
final inferred = inferredPositions[hopKey];
|
||||
if (inferred != null) _points.add(inferred);
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
hopLast = hopKey;
|
||||
}
|
||||
if (targetPos != null) {
|
||||
if (_targetContact != null && _targetContact!.type == advTypeChat) {
|
||||
@@ -583,7 +736,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
|
||||
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
|
||||
_mapKey = ValueKey(
|
||||
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}',
|
||||
'${context.l10n.pathTrace_you},${_traceData!.pathData.map(PathHelper.formatHopHex).join(',')}',
|
||||
);
|
||||
_pathDistanceMeters = getPathDistanceMeters(_points);
|
||||
_primaryOutboundHops = _outboundHops(pathData);
|
||||
@@ -605,37 +758,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
/// Outbound hop bytes of the traced path, mirroring the round-trip
|
||||
/// dedup logic used when building [_points].
|
||||
List<int> _outboundHops(Uint8List pathData) {
|
||||
final hops = <int>[];
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
List<Uint8List> _outboundHops(List<Uint8List> pathData) {
|
||||
final hops = <Uint8List>[];
|
||||
var hopLast = '';
|
||||
var hopLastLast = '';
|
||||
for (final hop in pathData) {
|
||||
if (hop == hopLastLast && widget.flipPathAround) break;
|
||||
final hopKey = _hopKey(hop);
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) break;
|
||||
hops.add(hop);
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
hopLast = hopKey;
|
||||
}
|
||||
return hops;
|
||||
}
|
||||
|
||||
Contact? _contactForHop(int hop, MeshCoreConnector connector) {
|
||||
final traced = _traceData?.pathContacts[hop];
|
||||
Contact? _contactForHop(Uint8List hop, MeshCoreConnector connector) {
|
||||
final traced = _traceData?.pathContacts[_hopKey(hop)];
|
||||
if (traced != null) return traced;
|
||||
for (final c in connector.allContactsUnfiltered) {
|
||||
if (c.type != advTypeChat &&
|
||||
c.publicKey.isNotEmpty &&
|
||||
c.publicKey[0] == hop) {
|
||||
c.publicKey.length >= hop.length &&
|
||||
listEquals(c.publicKey.sublist(0, hop.length), hop)) {
|
||||
return c;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng? _inferredPositionForHop(int hop, MeshCoreConnector connector) {
|
||||
final cached = _inferredHopPositions[hop];
|
||||
LatLng? _inferredPositionForHop(Uint8List hop, MeshCoreConnector connector) {
|
||||
final hopKey = _hopKey(hop);
|
||||
final cached = _inferredHopPositions[hopKey];
|
||||
if (cached != null) return cached;
|
||||
final peers = connector.contacts
|
||||
.where((c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop)
|
||||
.where(
|
||||
(c) =>
|
||||
c.hasLocation &&
|
||||
c.path.isNotEmpty &&
|
||||
_matchesHopPrefix(
|
||||
_lastHopChunk(c.path, widget.pathHashByteWidth),
|
||||
hop,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
if (peers.isEmpty) return null;
|
||||
final lat =
|
||||
@@ -643,10 +806,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final lon =
|
||||
peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length;
|
||||
final pos = LatLng(lat, lon);
|
||||
_inferredHopPositions[hop] = pos;
|
||||
_inferredHopPositions[hopKey] = pos;
|
||||
return pos;
|
||||
}
|
||||
|
||||
String _pathKeyForHops(List<Uint8List> hops) {
|
||||
return hops.map(PathHelper.formatHopHex).join(',');
|
||||
}
|
||||
|
||||
/// Rebuilds the renderable paths: the traced path as primary plus up to
|
||||
/// four distinct alternates from the target contact's path history.
|
||||
void _rebuildDisplayPaths(MeshCoreConnector connector) {
|
||||
@@ -664,18 +831,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final target = widget.targetContact;
|
||||
final history = _pathHistory;
|
||||
if (target != null && history != null) {
|
||||
final seen = <String>{_primaryOutboundHops.join(',')};
|
||||
final seen = <String>{_pathKeyForHops(_primaryOutboundHops)};
|
||||
var altIndex = 0;
|
||||
for (final record in history.getRecentPaths(target.publicKeyHex)) {
|
||||
if (record.pathBytes.isEmpty) continue;
|
||||
if (!seen.add(record.pathBytes.join(','))) continue;
|
||||
final recordHops = PathHelper.splitPathBytes(
|
||||
record.pathBytes,
|
||||
widget.pathHashByteWidth,
|
||||
);
|
||||
if (!seen.add(_pathKeyForHops(recordHops))) continue;
|
||||
if (altIndex >= kAlternatePathColors.length) break;
|
||||
final alt = _buildDisplayPath(
|
||||
id: 'alt-${record.pathBytes.join('-')}',
|
||||
id: 'alt-${_pathKeyForHops(recordHops)}',
|
||||
label: context.l10n.pathMap_alternate(altIndex + 1),
|
||||
color: kAlternatePathColors[altIndex],
|
||||
isPrimary: false,
|
||||
hops: record.pathBytes,
|
||||
hops: recordHops,
|
||||
record: record,
|
||||
connector: connector,
|
||||
);
|
||||
@@ -700,7 +871,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
required String label,
|
||||
required Color color,
|
||||
required bool isPrimary,
|
||||
required List<int> hops,
|
||||
required List<Uint8List> hops,
|
||||
required MeshCoreConnector connector,
|
||||
PathRecord? record,
|
||||
}) {
|
||||
@@ -719,7 +890,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
|
||||
for (var i = 0; i < hops.length; i++) {
|
||||
final hop = hops[i];
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
final contact = _contactForHop(hop, connector);
|
||||
LatLng? pos;
|
||||
var isGps = false;
|
||||
@@ -773,7 +944,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
label: label,
|
||||
color: color,
|
||||
isPrimary: isPrimary,
|
||||
hopBytes: List<int>.from(hops),
|
||||
hopBytes: List<Uint8List>.from(hops),
|
||||
points: points,
|
||||
pointLabels: labels,
|
||||
pointConfirmed: confirmed,
|
||||
@@ -934,29 +1105,34 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
List<Marker> _buildHopMarkers(
|
||||
List<int> pathData, {
|
||||
List<Uint8List> pathData, {
|
||||
required bool showLabels,
|
||||
required Contact? target,
|
||||
}) {
|
||||
final markers = <Marker>[];
|
||||
int hopLast = 0;
|
||||
int hopLastLast = 0;
|
||||
String hopLast = '';
|
||||
String hopLastLast = '';
|
||||
for (final hop in pathData) {
|
||||
final contact = _traceData!.pathContacts[hop];
|
||||
final inferred = _inferredHopPositions[hop];
|
||||
final hopKey = _hopKey(hop);
|
||||
final contact = _traceData!.pathContacts[hopKey];
|
||||
final inferred = _inferredHopPositions[hopKey];
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
if (hop == hopLastLast && widget.flipPathAround) {
|
||||
if (hopKey == hopLastLast && widget.flipPathAround) {
|
||||
continue; //skip duplicate hops in round-trip paths
|
||||
}
|
||||
if (!hasGps && inferred == null) {
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
hopLast = hopKey;
|
||||
continue; //skip hops with no GPS and no inferred position
|
||||
}
|
||||
final point = hasGps
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: inferred!;
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final label = PathHelper.formatHopHex(hop);
|
||||
final shortLabel = label.length > 2 ? label.substring(0, 2) : label;
|
||||
final fullLabel = label.length > 2
|
||||
? (contact?.name != null ? '$label: ${contact!.name}' : label)
|
||||
: (contact?.name ?? label);
|
||||
|
||||
markers.add(
|
||||
Marker(
|
||||
@@ -989,9 +1165,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
hasGps ? label : '~$label',
|
||||
shortLabel,
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 10,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: hasGps ? MeshPalette.signal : MeshPalette.warn,
|
||||
),
|
||||
@@ -1001,15 +1177,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
),
|
||||
);
|
||||
if (showLabels) {
|
||||
markers.add(
|
||||
_buildNodeLabelMarker(
|
||||
point: point,
|
||||
label: contact?.name ?? '~$label',
|
||||
),
|
||||
);
|
||||
markers.add(_buildNodeLabelMarker(point: point, label: fullLabel));
|
||||
}
|
||||
hopLastLast = hopLast;
|
||||
hopLast = hop;
|
||||
hopLast = hopKey;
|
||||
}
|
||||
|
||||
_addEndpointMarkers(markers, showLabels: showLabels, target: target);
|
||||
@@ -1137,17 +1308,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final markers = <Marker>[];
|
||||
|
||||
// Hop byte -> paths that use it, in display order.
|
||||
final hopPaths = <int, List<DisplayPath>>{};
|
||||
// Hop prefix -> paths that use it, in display order.
|
||||
final hopPaths = <String, List<DisplayPath>>{};
|
||||
final hopsByKey = <String, Uint8List>{};
|
||||
for (final path in _visiblePaths) {
|
||||
for (final hop in path.hopBytes) {
|
||||
final list = hopPaths.putIfAbsent(hop, () => []);
|
||||
final hopKey = _hopKey(hop);
|
||||
hopsByKey[hopKey] = hop;
|
||||
final list = hopPaths.putIfAbsent(hopKey, () => []);
|
||||
if (!list.contains(path)) list.add(path);
|
||||
}
|
||||
}
|
||||
|
||||
for (final entry in hopPaths.entries) {
|
||||
final hop = entry.key;
|
||||
final hopKey = entry.key;
|
||||
final hop = hopsByKey[hopKey];
|
||||
if (hop == null) continue;
|
||||
final paths = entry.value;
|
||||
final contact = _contactForHop(hop, connector);
|
||||
final hasGps = contact != null && contact.hasLocation;
|
||||
@@ -1155,7 +1331,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
? LatLng(contact.latitude!, contact.longitude!)
|
||||
: _inferredPositionForHop(hop, connector);
|
||||
if (point == null) continue;
|
||||
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final label = PathHelper.formatHopHex(hop);
|
||||
final baseColor = hasGps ? MeshPalette.signal : MeshPalette.warn;
|
||||
final shared = paths.length > 1;
|
||||
|
||||
@@ -1240,11 +1416,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
void _showSharedNodeSheet(
|
||||
int hop,
|
||||
Uint8List hop,
|
||||
Contact? contact,
|
||||
List<DisplayPath> paths,
|
||||
) {
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
showSharedNodeSheet(
|
||||
context,
|
||||
title:
|
||||
@@ -1291,29 +1467,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
String formatDirectionText(PathTraceData pathTraceData, int index) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (pathTraceData.pathData.isEmpty) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
if (index < 0 || index > pathTraceData.pathData.length) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
|
||||
if (index == 0 || index == pathTraceData.pathData.length) {
|
||||
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();
|
||||
final hop = pathTraceData.pathData.last;
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
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();
|
||||
final hop = pathTraceData.pathData[index - 1];
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
@@ -1321,14 +1496,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
}
|
||||
|
||||
String formatDirectionSubText(PathTraceData pathTraceData, int index) {
|
||||
if (index == 0 || index == pathTraceData.snrData.length - 1) {
|
||||
if (pathTraceData.pathData.isEmpty) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
if (index < 0 || index > pathTraceData.pathData.length) {
|
||||
return context.l10n.pathTrace_you;
|
||||
}
|
||||
|
||||
if (index == 0 || index == pathTraceData.pathData.length) {
|
||||
if (index == 0) {
|
||||
final contactName =
|
||||
pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name;
|
||||
final hex = pathTraceData.pathData[0]
|
||||
.toRadixString(16)
|
||||
.padLeft(2, '0')
|
||||
.toUpperCase();
|
||||
final hop = pathTraceData.pathData.first;
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
@@ -1336,12 +1515,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
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();
|
||||
final hop = pathTraceData.pathData[index];
|
||||
final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
return contactName != null
|
||||
? "$hex: $contactName"
|
||||
: "$hex: ${context.l10n.channelPath_unknownRepeater}";
|
||||
@@ -1399,7 +1575,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
},
|
||||
),
|
||||
children: [
|
||||
ThemedMapTileLayer(tileCache: tileCache),
|
||||
tileCache.buildTileLayer(context),
|
||||
AnimatedBuilder(
|
||||
animation: _playback,
|
||||
builder: (context, _) {
|
||||
@@ -1670,11 +1846,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final l10n = context.l10n;
|
||||
|
||||
final hopUseCount = <int, int>{};
|
||||
final hopUseCount = <String, int>{};
|
||||
if (_viewMode == PathViewMode.combined) {
|
||||
for (final p in _visiblePaths) {
|
||||
for (final hop in p.hopBytes.toSet()) {
|
||||
hopUseCount.update(hop, (v) => v + 1, ifAbsent: () => 1);
|
||||
for (final hopKey in p.hopBytes.map(PathHelper.formatHopHex).toSet()) {
|
||||
hopUseCount.update(hopKey, (v) => v + 1, ifAbsent: () => 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1690,7 +1866,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
Widget? trailing;
|
||||
if (index < path.hopBytes.length) {
|
||||
final hop = path.hopBytes[index];
|
||||
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||
final hex = PathHelper.formatHopHex(hop);
|
||||
final contact = _contactForHop(hop, connector);
|
||||
title = contact != null
|
||||
? '$hex: ${contact.name}'
|
||||
@@ -1703,7 +1879,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
|
||||
: inferred
|
||||
? l10n.pathTrace_legendInferred
|
||||
: l10n.pathMap_noLocation;
|
||||
final sharedCount = hopUseCount[hop] ?? 0;
|
||||
final sharedCount = hopUseCount[_hopKey(hop)] ?? 0;
|
||||
subtitle = sharedCount > 1
|
||||
? '$status · ${l10n.pathMap_sharedNodeCount(sharedCount)}'
|
||||
: status;
|
||||
|
||||
@@ -0,0 +1,527 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/contact.dart';
|
||||
import 'package:meshcore_open/storage/region_store.dart';
|
||||
import 'package:meshcore_open/theme/mesh_theme.dart';
|
||||
import 'package:meshcore_open/widgets/mesh_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
Future<void> pushRegionManagementScreen(BuildContext context) {
|
||||
return Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute<void>(
|
||||
builder: (context) => const RegionManagementScreen(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class RegionManagementScreen extends StatefulWidget {
|
||||
const RegionManagementScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
|
||||
}
|
||||
|
||||
class _RegionManagementScreenState extends State<RegionManagementScreen> {
|
||||
static final RegExp _validFetchedRegion = RegExp(r'^[a-z0-9-]{1,30}$');
|
||||
|
||||
final RegionStore _regionStore = RegionStore();
|
||||
List<Region> _regions = [];
|
||||
bool _isFetchingRegions = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
void _loadRegions() {
|
||||
final regions = _regionStore.loadRegions();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_regions = regions;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.settings_regionManagement_screenTitle),
|
||||
centerTitle: true,
|
||||
actions: [
|
||||
IconButton(
|
||||
tooltip: l10n.settings_regionAddRegion,
|
||||
icon: const Icon(Icons.add),
|
||||
onPressed: () => _showAddRegionDialog(context),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: l10n.settings_regionFetchRegions,
|
||||
icon: _isFetchingRegions
|
||||
? const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.travel_explore),
|
||||
onPressed: _isFetchingRegions ? null : _showFetchRegionsDialog,
|
||||
),
|
||||
],
|
||||
),
|
||||
body: ListView.builder(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
|
||||
itemCount: _regions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final region = _regions[index];
|
||||
return _buildRegionTile(context, region);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showAddRegionDialog(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final controller = TextEditingController();
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(l10n.settings_regionName),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (_) => _handleAddRegion(controller.text, context),
|
||||
decoration: InputDecoration(
|
||||
hintText: l10n.settings_regionNameHint,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
inputFormatters: <TextInputFormatter>[
|
||||
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
|
||||
],
|
||||
maxLength: 30,
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => _handleAddRegion(controller.text, context),
|
||||
child: Text(l10n.common_add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _showFetchRegionsDialog() async {
|
||||
if (_isFetchingRegions) return;
|
||||
|
||||
setState(() {
|
||||
_isFetchingRegions = true;
|
||||
});
|
||||
|
||||
Set<Region> fetchedRegions = {};
|
||||
try {
|
||||
fetchedRegions = await _fetchRegionsFromRepeaters();
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isFetchingRegions = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
final l10n = context.l10n;
|
||||
final sortedRegions = fetchedRegions.toList()..sort();
|
||||
await showDialog<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(l10n.settings_regionFetchRegions),
|
||||
content: sortedRegions.isEmpty
|
||||
? Text(l10n.settings_regionFetchRegionsFail)
|
||||
: StatefulBuilder(
|
||||
builder: (context, setDialogState) {
|
||||
return SizedBox(
|
||||
width: double.maxFinite,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
itemCount: sortedRegions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final fetchedRegion = sortedRegions[index];
|
||||
final alreadyExists = _regions.contains(fetchedRegion);
|
||||
return MeshCard(
|
||||
margin: const EdgeInsets.symmetric(vertical: 4),
|
||||
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.landscape,
|
||||
color: MeshPalette.blue,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fetchedRegion,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
style: alreadyExists
|
||||
? TextButton.styleFrom(
|
||||
foregroundColor: Theme.of(
|
||||
context,
|
||||
).disabledColor,
|
||||
)
|
||||
: null,
|
||||
onPressed: () {
|
||||
if (alreadyExists) {
|
||||
_showDialogSnackBar(
|
||||
context,
|
||||
l10n.settings_regionFetchRegionsAlreadyExists,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_regionStore.addRegion(fetchedRegion);
|
||||
_loadRegions();
|
||||
setDialogState(() {});
|
||||
},
|
||||
child: Text(l10n.common_add),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(l10n.common_close),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showDialogSnackBar(BuildContext context, String message) {
|
||||
final overlay = Overlay.maybeOf(context);
|
||||
if (overlay == null) return;
|
||||
|
||||
final theme = Theme.of(context);
|
||||
final entry = OverlayEntry(
|
||||
builder: (context) => Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 32,
|
||||
child: SafeArea(
|
||||
child: Material(
|
||||
color: theme.colorScheme.inverseSurface,
|
||||
elevation: 6,
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||||
child: Text(
|
||||
message,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: theme.colorScheme.onInverseSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
overlay.insert(entry);
|
||||
Timer(const Duration(seconds: 3), entry.remove);
|
||||
}
|
||||
|
||||
Future<Set<Region>> _fetchRegionsFromRepeaters() async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
final repeaters = await _discoverNearbyRepeaters(connector);
|
||||
final regions = <Region>{};
|
||||
|
||||
for (final repeater in repeaters) {
|
||||
if (!mounted || !connector.isConnected) break;
|
||||
regions.addAll(await _requestRegionsFromRepeater(connector, repeater));
|
||||
}
|
||||
|
||||
return regions;
|
||||
}
|
||||
|
||||
Future<List<Contact>> _discoverNearbyRepeaters(
|
||||
MeshCoreConnector connector,
|
||||
) async {
|
||||
final repeaters = connector.contacts
|
||||
.where((contact) => contact.type == advTypeRepeater)
|
||||
.toList();
|
||||
if (repeaters.isEmpty || !connector.isConnected) return <Contact>[];
|
||||
|
||||
StreamSubscription<Uint8List>? subscription;
|
||||
Timer? timeout;
|
||||
final completer = Completer<Set<String>>();
|
||||
final respondingPrefixes = <String>{};
|
||||
final tag = DateTime.now().microsecondsSinceEpoch & 0xFFFFFFFF;
|
||||
|
||||
void complete() {
|
||||
if (completer.isCompleted) return;
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
completer.complete(respondingPrefixes);
|
||||
}
|
||||
|
||||
subscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty || completer.isCompleted) return;
|
||||
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
if (reader.readByte() != pushCodeControlData) return;
|
||||
if (reader.remaining < 9) return;
|
||||
reader.skipBytes(3); // SNR, RSSI, path_len from companion firmware.
|
||||
|
||||
final payloadType = reader.readByte();
|
||||
if (((payloadType >> 4) & 0x0F) != controlSubtypeDiscoverResp ||
|
||||
(payloadType & 0x0F) != advTypeRepeater) {
|
||||
return;
|
||||
}
|
||||
|
||||
reader.skipBytes(1); // Inbound SNR reported by the responding repeater.
|
||||
if (reader.readUInt32LE() != tag) return;
|
||||
|
||||
final publicKeyPrefix = reader.readRemainingBytes();
|
||||
if (publicKeyPrefix.isEmpty) return;
|
||||
respondingPrefixes.add(pubKeyToHex(publicKeyPrefix));
|
||||
} catch (_) {
|
||||
// Ignore malformed discovery frames; another response may still arrive.
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final payload = buildDiscoveryRequestPayload(tag, prefixOnly: true);
|
||||
await connector.sendFrame(buildSendControlDataFrame(payload));
|
||||
timeout = Timer(const Duration(seconds: 10), complete);
|
||||
final prefixes = await completer.future;
|
||||
return repeaters.where((contact) {
|
||||
final contactKey = contact.publicKeyHex.toLowerCase();
|
||||
return prefixes.any((prefix) => contactKey.startsWith(prefix));
|
||||
}).toList();
|
||||
} catch (_) {
|
||||
timeout?.cancel();
|
||||
await subscription.cancel();
|
||||
return <Contact>[];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<Region>> _requestRegionsFromRepeater(
|
||||
MeshCoreConnector connector,
|
||||
Contact repeater,
|
||||
) async {
|
||||
StreamSubscription<Uint8List>? subscription;
|
||||
Timer? timeout;
|
||||
final completer = Completer<Set<Region>>();
|
||||
int? expectedTag;
|
||||
final originalPath = Uint8List.fromList(repeater.path);
|
||||
final originalPathLength = repeater.pathLength;
|
||||
var pathChangedForRequest = false;
|
||||
|
||||
void complete(Set<Region> regions) {
|
||||
if (completer.isCompleted) return;
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
completer.complete(regions);
|
||||
}
|
||||
|
||||
void restartTimeout(Duration duration) {
|
||||
timeout?.cancel();
|
||||
timeout = Timer(duration, () => complete(<Region>{}));
|
||||
}
|
||||
|
||||
try {
|
||||
final replyPath = Uint8List(0);
|
||||
const replyHopCount = 0;
|
||||
await connector.setContactPath(repeater, replyPath, replyHopCount);
|
||||
pathChangedForRequest = true;
|
||||
|
||||
subscription = connector.receivedFrames.listen((frame) {
|
||||
if (frame.isEmpty || completer.isCompleted) return;
|
||||
|
||||
final reader = BufferReader(frame);
|
||||
try {
|
||||
final cmd = reader.readByte();
|
||||
if (cmd == respCodeSent) {
|
||||
reader.skipBytes(1);
|
||||
expectedTag = reader.readUInt32LE();
|
||||
final estimatedTimeoutMs = reader.readUInt32LE();
|
||||
restartTimeout(
|
||||
Duration(
|
||||
milliseconds: estimatedTimeoutMs > 0
|
||||
? estimatedTimeoutMs + 2000
|
||||
: 10000,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd == respCodeErr) {
|
||||
complete(<Region>{});
|
||||
return;
|
||||
}
|
||||
|
||||
if (cmd != pushCodeBinaryResponse || expectedTag == null) return;
|
||||
|
||||
reader.skipBytes(1);
|
||||
final tag = reader.readUInt32LE();
|
||||
if (tag != expectedTag) return;
|
||||
|
||||
complete(_parseRegionsResponse(reader.readRemainingBytes()));
|
||||
} catch (_) {
|
||||
complete(<Region>{});
|
||||
}
|
||||
});
|
||||
|
||||
restartTimeout(const Duration(seconds: 10));
|
||||
final frame = buildSendAnonReqFrame(
|
||||
repeater.publicKey,
|
||||
requestType: anonReqTypeRegions,
|
||||
replyPath: replyPath,
|
||||
replyHopCount: replyHopCount,
|
||||
pathHashWidth: connector.pathHashByteWidth,
|
||||
);
|
||||
await connector.sendFrame(frame);
|
||||
final regions = await completer.future;
|
||||
if (pathChangedForRequest && connector.isConnected) {
|
||||
await _restoreRepeaterPath(
|
||||
connector,
|
||||
repeater,
|
||||
originalPathLength,
|
||||
originalPath,
|
||||
);
|
||||
}
|
||||
return regions;
|
||||
} catch (_) {
|
||||
timeout?.cancel();
|
||||
subscription?.cancel();
|
||||
if (pathChangedForRequest && connector.isConnected) {
|
||||
await _restoreRepeaterPath(
|
||||
connector,
|
||||
repeater,
|
||||
originalPathLength,
|
||||
originalPath,
|
||||
);
|
||||
}
|
||||
return <Region>{};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _restoreRepeaterPath(
|
||||
MeshCoreConnector connector,
|
||||
Contact repeater,
|
||||
int originalPathLength,
|
||||
Uint8List originalPath,
|
||||
) async {
|
||||
if (originalPathLength < 0) {
|
||||
await connector.clearContactPath(repeater);
|
||||
return;
|
||||
}
|
||||
await connector.setContactPath(repeater, originalPath, originalPathLength);
|
||||
}
|
||||
|
||||
Set<Region> _parseRegionsResponse(Uint8List frame) {
|
||||
if (frame.length <= 4) return <Region>{};
|
||||
final names = utf8
|
||||
.decode(frame.sublist(4), allowMalformed: true)
|
||||
.replaceAll('\x00', '')
|
||||
.split(',');
|
||||
return names
|
||||
.map((name) => name.trim())
|
||||
.where((name) => _validFetchedRegion.hasMatch(name))
|
||||
.toSet();
|
||||
}
|
||||
|
||||
void _handleAddRegion(Region region, BuildContext context) {
|
||||
Navigator.pop(context);
|
||||
_regionStore.addRegion(region);
|
||||
_loadRegions();
|
||||
}
|
||||
|
||||
Widget _buildRegionTile(BuildContext context, Region region) {
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
return MeshCard(
|
||||
key: ValueKey(region),
|
||||
padding: const EdgeInsets.only(left: 14, right: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.landscape, color: MeshPalette.blue),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
region,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
tooltip: context.l10n.settings_deleteRegion,
|
||||
icon: Icon(Icons.delete_outline, color: scheme.error),
|
||||
onPressed: () => _confirmDelete(context, region),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _confirmDelete(BuildContext context, Region region) {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(context.l10n.settings_deleteRegion),
|
||||
content: Text(context.l10n.settings_deleteRegionConfirm(region)),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(context.l10n.common_cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final connector = context.read<MeshCoreConnector>();
|
||||
Navigator.pop(dialogContext);
|
||||
await _regionStore.removeRegion(region);
|
||||
// Deleting a region clears it from any channels that used it;
|
||||
// refresh the connector's in-memory channel regions to match.
|
||||
await connector.loadChannelSettings();
|
||||
_loadRegions();
|
||||
if (!context.mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(context.l10n.settings_regionDeleted)),
|
||||
);
|
||||
},
|
||||
child: Text(
|
||||
context.l10n.common_delete,
|
||||
style: TextStyle(
|
||||
color: Theme.of(dialogContext).colorScheme.error,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_protocol.dart';
|
||||
import 'package:meshcore_open/connector/meshcore_connector.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -31,6 +32,7 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
final l10n = context.l10n;
|
||||
final scheme = Theme.of(context).colorScheme;
|
||||
final settingsService = context.watch<AppSettingsService>();
|
||||
final connector = context.watch<MeshCoreConnector>();
|
||||
final chemistry = settingsService.batteryChemistryForRepeater(
|
||||
repeater.publicKeyHex,
|
||||
);
|
||||
@@ -85,7 +87,10 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
repeater.pathLabel(l10n),
|
||||
repeater.pathLabel(
|
||||
l10n,
|
||||
pathHashByteWidth: connector.pathHashByteWidth,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodySmall
|
||||
?.copyWith(color: scheme.onSurfaceVariant),
|
||||
),
|
||||
@@ -160,6 +165,10 @@ class RepeaterHubScreen extends StatelessWidget {
|
||||
value: 'lipo',
|
||||
child: Text(l10n.appSettings_batteryLipo),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 'lipo_hv',
|
||||
child: Text(l10n.appSettings_batteryLipoHv),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -11,6 +12,7 @@ import '../services/storage_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/mesh_ui.dart';
|
||||
import '../widgets/routing_sheet.dart';
|
||||
import '../utils/keys.dart';
|
||||
import '../helpers/snack_bar_builder.dart';
|
||||
|
||||
class RepeaterSettingsScreen extends StatefulWidget {
|
||||
@@ -44,6 +46,7 @@ enum _SettingField {
|
||||
advertInterval,
|
||||
floodAdvertInterval,
|
||||
pathHashMode,
|
||||
prvKey,
|
||||
txDelay,
|
||||
directTxDelay,
|
||||
intThresh,
|
||||
@@ -109,6 +112,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
bool _refreshingIntThresh = false;
|
||||
bool _refreshingAgcResetInterval = false;
|
||||
bool _runningAction = false;
|
||||
bool _searchingForKeyPair = false;
|
||||
bool _stopSearchingForKeyPair = false;
|
||||
StreamSubscription<Uint8List>? _frameSubscription;
|
||||
RepeaterCommandService? _commandService;
|
||||
|
||||
@@ -154,6 +159,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
|
||||
// Advanced
|
||||
int _pathHashMode = 0; // 0-2
|
||||
final TextEditingController _prvKeyController = TextEditingController();
|
||||
final TextEditingController _pubKeyController = TextEditingController();
|
||||
final TextEditingController _prvKeyPrefixController = TextEditingController();
|
||||
final KeyPairSearcher _keyPairSearcher = KeyPairSearcher();
|
||||
final int _searchChunkTime = 30; // time in ms to search before yielding
|
||||
final TextEditingController _txDelayController = TextEditingController();
|
||||
final TextEditingController _directTxDelayController =
|
||||
TextEditingController();
|
||||
@@ -206,6 +216,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_txDelayController.dispose();
|
||||
_directTxDelayController.dispose();
|
||||
_intThreshController.dispose();
|
||||
_prvKeyController.dispose();
|
||||
_pubKeyController.dispose();
|
||||
_prvKeyPrefixController.dispose();
|
||||
_stopSearchingForKeyPair = true;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -860,6 +874,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
command: 'set path.hash.mode $_pathHashMode',
|
||||
));
|
||||
}
|
||||
if (_dirtyFields.contains(_SettingField.prvKey)) {
|
||||
final v = _prvKeyController.text.trim();
|
||||
if (v != "") {
|
||||
pending.add((field: _SettingField.prvKey, command: 'set prv.key $v'));
|
||||
}
|
||||
}
|
||||
if (_dirtyFields.contains(_SettingField.txDelay) &&
|
||||
_txDelayController.text.isNotEmpty) {
|
||||
final v = double.tryParse(_txDelayController.text.trim());
|
||||
@@ -1005,6 +1025,36 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
void _startSearchingForKeyPair() async {
|
||||
if (_searchingForKeyPair) return;
|
||||
setState(() {
|
||||
_searchingForKeyPair = true;
|
||||
_stopSearchingForKeyPair = false;
|
||||
});
|
||||
_searchForKeyPair();
|
||||
}
|
||||
|
||||
void _searchForKeyPair() async {
|
||||
if (_stopSearchingForKeyPair) {
|
||||
if (mounted) setState(() => _searchingForKeyPair = false);
|
||||
return;
|
||||
}
|
||||
final String prefix = _prvKeyPrefixController.text.trim();
|
||||
MCKeyPair? keys = await _keyPairSearcher.findMatchingKeyPair(
|
||||
prefix,
|
||||
_searchChunkTime,
|
||||
);
|
||||
if (keys == null) {
|
||||
// Ran out of time; schedule another attempt.
|
||||
Timer.run(_searchForKeyPair);
|
||||
} else {
|
||||
setState(() => _searchingForKeyPair = false);
|
||||
_prvKeyController.text = pubKeyToHex(keys.private);
|
||||
_pubKeyController.text = pubKeyToHex(keys.public);
|
||||
_markChanged(_SettingField.prvKey);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildInlineRefreshButton({
|
||||
required bool isRefreshing,
|
||||
required VoidCallback onRefresh,
|
||||
@@ -1069,6 +1119,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
_buildOwnerInfoCard(),
|
||||
_buildActionsCard(),
|
||||
_buildAdvancedCard(),
|
||||
_buildKeysCard(),
|
||||
const SizedBox(height: 16),
|
||||
_buildDangerZoneCard(),
|
||||
],
|
||||
@@ -1984,6 +2035,127 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildKeysCard() {
|
||||
final l10n = context.l10n;
|
||||
return MeshCard(
|
||||
child: ExpansionTile(
|
||||
leading: const Icon(Icons.vpn_key),
|
||||
title: Text(
|
||||
l10n.repeater_keySettings,
|
||||
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
|
||||
),
|
||||
subtitle: Text(l10n.repeater_keySettingsSubtitle),
|
||||
childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4),
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _prvKeyController,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
|
||||
],
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_prvKey,
|
||||
helperText: l10n.repeater_prvKeyHelper,
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
onChanged: (_) {
|
||||
_pubKeyController.text = "";
|
||||
if (_prvKeyController.text.isNotEmpty) {
|
||||
_markChanged(_SettingField.prvKey);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8),
|
||||
child: _searchingForKeyPair
|
||||
? IconButton(
|
||||
icon: const Icon(Icons.cancel, size: 24),
|
||||
onPressed: (() => _stopSearchingForKeyPair = true),
|
||||
tooltip: l10n.repeater_stopGeneratingPrvKey,
|
||||
visualDensity: VisualDensity.compact,
|
||||
)
|
||||
: IconButton(
|
||||
icon: const Icon(Icons.casino_rounded, size: 24),
|
||||
onPressed: () {
|
||||
_startSearchingForKeyPair();
|
||||
},
|
||||
tooltip: l10n.repeater_generatePrvKey,
|
||||
visualDensity: VisualDensity.compact,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
readOnly: true,
|
||||
controller: _pubKeyController,
|
||||
style: TextStyle(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).textTheme.headlineSmall?.color?.withValues(alpha: 0.6),
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pubKey,
|
||||
helperText: l10n.repeater_pubKeyHelper,
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: _searchingForKeyPair
|
||||
? CircularProgressIndicator(strokeWidth: 2)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _prvKeyPrefixController,
|
||||
maxLength: 4,
|
||||
inputFormatters: [
|
||||
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
|
||||
],
|
||||
onChanged: (_) => setState(() => {}), // updates helper text
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pubKeyPrefix,
|
||||
helperText: l10n.repeater_pubKeyPrefixHelper(
|
||||
pow(16, _prvKeyPrefixController.text.length).toInt(),
|
||||
),
|
||||
helperMaxLines: 3,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 48),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDangerZoneCard() {
|
||||
final l10n = context.l10n;
|
||||
return MeshCard(
|
||||
|
||||
@@ -9,6 +9,7 @@ import '../connector/meshcore_connector.dart';
|
||||
import '../connector/meshcore_protocol.dart';
|
||||
import '../l10n/l10n.dart';
|
||||
import '../models/radio_settings.dart';
|
||||
import '../services/app_settings_service.dart';
|
||||
import '../services/app_debug_log_service.dart';
|
||||
import '../theme/mesh_theme.dart';
|
||||
import '../widgets/app_bar.dart';
|
||||
@@ -19,6 +20,7 @@ import 'app_debug_log_screen.dart';
|
||||
import 'ble_debug_log_screen.dart';
|
||||
import '../widgets/radio_stats_entry.dart';
|
||||
import '../widgets/sync_progress_overlay.dart';
|
||||
import 'region_management_screen.dart';
|
||||
|
||||
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
|
||||
/// to the UI enum range (always 5-8).
|
||||
@@ -278,6 +280,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
value: connector.deviceIdLabel,
|
||||
),
|
||||
_buildBatteryInfoRow(context, connector),
|
||||
if (connector.manufacturerName != null)
|
||||
_infoRow(
|
||||
context,
|
||||
label: l10n.settings_infoHardware,
|
||||
value: connector.manufacturerName!,
|
||||
),
|
||||
if (connector.firmwareVersionString != null)
|
||||
_infoRow(
|
||||
context,
|
||||
label: l10n.settings_infoFirmware,
|
||||
value: connector.firmwareVersionString!,
|
||||
),
|
||||
if (connector.selfName != null)
|
||||
_infoRow(
|
||||
context,
|
||||
@@ -449,6 +463,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
onTap: () => _showRadioSettings(context, connector),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.landscape,
|
||||
title: l10n.settings_regionSettings,
|
||||
subtitle: l10n.settings_regionSettingsSubtitle,
|
||||
onTap: () => pushRegionManagementScreen(context),
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.sensors_outlined,
|
||||
@@ -458,10 +480,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
? () => pushCompanionRadioStatsScreen(context)
|
||||
: null,
|
||||
),
|
||||
const Divider(height: 1, indent: 16),
|
||||
_tappableTile(
|
||||
context,
|
||||
icon: Icons.route_outlined,
|
||||
title: l10n.repeater_pathHashMode,
|
||||
subtitle: _pathHashModeSubtitle(context, connector.pathHashByteWidth),
|
||||
onTap: connector.isConnected
|
||||
? () => _editPathHashMode(context, connector)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _pathHashModeSubtitle(BuildContext context, int pathHashByteWidth) {
|
||||
final l10n = context.l10n;
|
||||
return switch (pathHashByteWidth.clamp(1, 4).toInt()) {
|
||||
1 => l10n.repeater_pathHashModeOption0,
|
||||
2 => l10n.repeater_pathHashModeOption1,
|
||||
3 => l10n.repeater_pathHashModeOption2,
|
||||
_ => l10n.repeater_pathHashModeOption3,
|
||||
};
|
||||
}
|
||||
|
||||
Widget _buildLocationCardContent(
|
||||
BuildContext context,
|
||||
MeshCoreConnector connector,
|
||||
@@ -750,8 +792,90 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
);
|
||||
}
|
||||
|
||||
void _editPathHashMode(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
var selectedMode = (connector.pathHashByteWidth - 1).clamp(0, 3).toInt();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (dialogContext) => StatefulBuilder(
|
||||
builder: (context, setDialogState) => AlertDialog(
|
||||
title: Text(l10n.repeater_pathHashMode),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
DropdownButtonFormField<int>(
|
||||
initialValue: selectedMode,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.repeater_pathHashMode,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
items: [
|
||||
DropdownMenuItem(
|
||||
value: 0,
|
||||
child: Text(l10n.repeater_pathHashModeOption0),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 1,
|
||||
child: Text(l10n.repeater_pathHashModeOption1),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 2,
|
||||
child: Text(l10n.repeater_pathHashModeOption2),
|
||||
),
|
||||
DropdownMenuItem(
|
||||
value: 3,
|
||||
child: Text(l10n.repeater_pathHashModeOption3),
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
if (value == null) return;
|
||||
setDialogState(() => selectedMode = value);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
l10n.repeater_pathHashModeHelper,
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(dialogContext),
|
||||
child: Text(l10n.common_cancel),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () async {
|
||||
Navigator.pop(dialogContext);
|
||||
try {
|
||||
await connector.setPathHashMode(selectedMode);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.repeater_settingsSaved),
|
||||
);
|
||||
} catch (e) {
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
context,
|
||||
content: Text(l10n.settings_error(e.toString())),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Text(l10n.common_save),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _editLocation(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
final latController = TextEditingController();
|
||||
final lonController = TextEditingController();
|
||||
final intervalController = TextEditingController();
|
||||
@@ -763,9 +887,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
final bool hasGPS = customVars.containsKey("gps");
|
||||
bool isGPSEnabled = customVars["gps"] == "1";
|
||||
|
||||
// Read current interval or default to 900 (15 minutes)
|
||||
final currentInterval =
|
||||
int.tryParse(customVars["gps_interval"] ?? "") ?? 900;
|
||||
final currentInterval = settingsService.resolvedGpsIntervalSeconds(
|
||||
customVars,
|
||||
);
|
||||
intervalController.text = currentInterval.toString();
|
||||
|
||||
String? intervalError;
|
||||
@@ -863,7 +987,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
Navigator.pop(context);
|
||||
|
||||
if (interval != null) {
|
||||
await connector.setCustomVar("gps_interval:$interval");
|
||||
await settingsService.setGpsIntervalSeconds(
|
||||
interval,
|
||||
writeToDevice: (value) =>
|
||||
connector.setCustomVar("gps_interval:$value"),
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
@@ -1163,12 +1291,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
|
||||
|
||||
void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
final l10n = context.l10n;
|
||||
final settingsService = context.read<AppSettingsService>();
|
||||
|
||||
int telemetryMode = connector.telemetryModeBase;
|
||||
int telemetryLocMode = connector.telemetryModeLoc;
|
||||
int telemetryEnvMode = connector.telemetryModeEnv;
|
||||
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
|
||||
int multiAcks = connector.multiAcks;
|
||||
bool autoZeroHopAdvertOnGpsUpdate =
|
||||
settingsService.settings.autoSendZeroHopAdvertOnGpsUpdate;
|
||||
|
||||
final telemModeBase = [
|
||||
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
|
||||
@@ -1203,6 +1334,20 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
FeatureToggleRow(
|
||||
title: l10n.settings_autoZeroHopAdvertOnGpsUpdate,
|
||||
subtitle: l10n.settings_autoZeroHopAdvertOnGpsUpdateSubtitle,
|
||||
value: autoZeroHopAdvertOnGpsUpdate,
|
||||
enabled: advertLocPolicy,
|
||||
onChanged: advertLocPolicy
|
||||
? (value) {
|
||||
setDialogState(
|
||||
() => autoZeroHopAdvertOnGpsUpdate = value,
|
||||
);
|
||||
}
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwitchListTile(
|
||||
title: Text(l10n.settings_multiAck),
|
||||
value: multiAcks == 1,
|
||||
@@ -1271,6 +1416,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
|
||||
advertLocPolicy ? 1 : 0,
|
||||
multiAcks,
|
||||
);
|
||||
await settingsService.setAutoSendZeroHopAdvertOnGpsUpdate(
|
||||
autoZeroHopAdvertOnGpsUpdate,
|
||||
);
|
||||
await connector.refreshDeviceInfo();
|
||||
if (!context.mounted) return;
|
||||
showDismissibleSnackBar(
|
||||
|
||||