Compare commits
129 Commits
ui
..
PRE-BETA-9.5.1
| 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 | |||
| 9957e1fc1f | |||
| e38d03a32e | |||
| ea657a964a | |||
| 760d8e1db3 | |||
| 880df84828 | |||
| 815534d409 | |||
| 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"
|
||||
@@ -41,7 +41,7 @@ lib/
|
||||
├── models/ # Plain data classes (Contact, Channel, Message, Community, …)
|
||||
├── services/ # ChangeNotifier services + IO services (retry, translation, ML, …)
|
||||
├── storage/ # SharedPreferences-backed stores, scoped per device key
|
||||
├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers)
|
||||
├── helpers/ # Pure utilities (Smaz compression, GIF parsing, scroll helpers, path hop resolution)
|
||||
├── utils/ # Platform / IO / UX utilities (logger, GPX export, dialogs)
|
||||
├── theme/ # MeshPalette (defined, not yet wired in main.dart)
|
||||
├── l10n/ # ARB localization for 18 locales
|
||||
@@ -194,14 +194,14 @@ enum MeshCoreConnectionState {
|
||||
|
||||
## Dependencies
|
||||
|
||||
App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|
||||
App version: `9.5.0+13` — Dart SDK constraint: `^3.9.2`
|
||||
|
||||
**Connectivity**
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| flutter_blue_plus | ^2.1.0 | BLE scanning, connecting, and UART data transfer |
|
||||
| flutter_blue_plus_platform_interface | ^8.2.1 | Platform-interface layer required by flutter_blue_plus |
|
||||
| flutter_blue_plus_platform_interface | ^9.0.2 | Platform-interface layer required by flutter_blue_plus |
|
||||
| flserial | git (MeshEnvy fork) | USB serial transport for wired device connections (TODO: upstream pending) |
|
||||
|
||||
**State / Storage**
|
||||
@@ -232,7 +232,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| material_symbols_icons | ^4.2906.0 | Extended Material Symbols icon set (line-of-sight, etc.) |
|
||||
| material_symbols_icons | ^4.2928.1 | Extended Material Symbols icon set (line-of-sight, etc.) |
|
||||
| flutter_svg | ^2.0.10+1 | Renders SVG assets (custom icons such as LoS indicator) |
|
||||
| cached_network_image | ^3.4.1 | Caches map tile images downloaded over the network |
|
||||
| flutter_cache_manager | ^3.4.1 | Underlying cache manager used by cached_network_image |
|
||||
@@ -246,7 +246,7 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|
||||
|
||||
| Package | Version | Purpose |
|
||||
|---------|---------|---------|
|
||||
| flutter_local_notifications | ^20.1.0 | Shows local push notifications for incoming messages |
|
||||
| flutter_local_notifications | ^22.0.0 | Shows local push notifications for incoming messages |
|
||||
| flutter_foreground_task | ^9.2.0 | Keeps the app alive in background to maintain BLE/USB connection |
|
||||
|
||||
**ML / AI**
|
||||
@@ -255,7 +255,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|
||||
|---------|---------|---------|
|
||||
| ml_algo | ^16.0.0 | OLS regression used in `timeout_prediction_service.dart` to predict message ACK timeouts |
|
||||
| ml_dataframe | ^1.0.0 | DataFrame input format required by ml_algo |
|
||||
| llamadart | >=0.6.8 <0.7.0 | On-device LLM inference used in `translation_service.dart` for message translation |
|
||||
| llamadart | ^0.8.0 | On-device LLM inference used in `translation_service.dart` for message translation |
|
||||
| flutter_langdetect | ^0.0.1 | Detects a message's source language in `translation_service.dart` before translating |
|
||||
|
||||
**Misc**
|
||||
|
||||
@@ -263,8 +264,8 @@ App version: `8.0.0+11` — Dart SDK constraint: `^3.9.2`
|
||||
|---------|---------|---------|
|
||||
| http | ^1.2.0 | Fetches tile URLs and any remote API calls |
|
||||
| url_launcher | ^6.3.0 | Opens URLs in the system browser from linkified chat text |
|
||||
| share_plus | ^12.0.1 | Shares files (e.g. exported GPX tracks) via the system share sheet |
|
||||
| package_info_plus | ^9.0.0 | Reads app version/build number displayed in settings |
|
||||
| share_plus | ^13.1.0 | Shares files (e.g. exported GPX tracks) via the system share sheet |
|
||||
| package_info_plus | ^10.1.0 | Reads app version/build number displayed in settings |
|
||||
| web | ^1.1.1 | Web-platform APIs for USB serial and browser detection on Flutter Web |
|
||||
| intl | any | Internationalization and locale formatting (required by flutter_localizations) |
|
||||
| build_pipe | ^0.3.1 | CI/CD build pipeline configuration (web release builds with versioned assets) |
|
||||
@@ -333,4 +334,4 @@ PWA scaffold present but boilerplate (`manifest.json` and `index.html` are unmod
|
||||
| `lib/services/translation_service.dart` | On-device LLM translation (llamadart) |
|
||||
| `lib/storage/prefs_manager.dart` | SharedPreferences singleton initialized in `main()` |
|
||||
| `lib/screens/scanner_screen.dart` | Home screen — BLE scan and connect |
|
||||
| `pubspec.yaml` | Dependencies and project metadata (current version `8.0.0+11`) |
|
||||
| `pubspec.yaml` | Dependencies and project metadata (current version `9.5.0+13`) |
|
||||
|
||||
@@ -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
|
||||
@@ -94,12 +94,12 @@ MeshCore Open is a cross-platform mobile application for communicating with Mesh
|
||||
|---------|---------|
|
||||
| flutter_blue_plus | Bluetooth Low Energy communication |
|
||||
| provider | State management |
|
||||
| sqflite | Local database storage |
|
||||
| shared_preferences | Local key-value storage (scoped per device) |
|
||||
| flutter_map | Interactive map display |
|
||||
| latlong2 | Geographic coordinate handling |
|
||||
| flutter_local_notifications | Background notification support |
|
||||
| smaz | Message compression |
|
||||
| pointycastle | Cryptographic operations |
|
||||
| llamadart | On-device LLM message translation |
|
||||
| intl | Internationalization and date formatting |
|
||||
|
||||
## Getting Started
|
||||
@@ -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>
|
||||
@@ -1,3 +1,7 @@
|
||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||
android.useAndroidX=true
|
||||
android.enableJetifier=true
|
||||
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||
android.builtInKotlin=false
|
||||
# This newDsl flag was added automatically by Flutter migrator
|
||||
android.newDsl=false
|
||||
|
||||
@@ -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)
|
||||
|
||||
---
|
||||
@@ -198,6 +198,7 @@ Tap the translate button on any received message. On first use, the GGUF model f
|
||||
|
||||
### How It Works
|
||||
- Model files are managed by `TranslationFileStore`; download progress is shown in-place
|
||||
- Before translating, the source language is automatically detected using the `flutter_langdetect` package. If the detected language already matches the target language, translation is skipped
|
||||
- Translation runs via `TranslationService` using the llamadart CPU backend (arm64 and x64 on Android)
|
||||
- Translated text is shown in `TranslatedMessageContent` as an inline overlay on the original message bubble
|
||||
- Each translation is cached; re-tapping shows the cached result without re-running inference
|
||||
|
||||
@@ -56,6 +56,7 @@ enum MeshCoreConnectionState {
|
||||
- `Lilygo`
|
||||
- `HT-`
|
||||
- `LowMesh_MC_`
|
||||
- `NRF52`
|
||||
2. **Connect** with 15-second timeout (6 seconds on Linux)
|
||||
3. **Request MTU** 185 bytes (non-web only)
|
||||
4. **Discover services** and locate NUS
|
||||
@@ -115,13 +116,17 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 32 | CMD_SET_CHANNEL | Set channel name and PSK |
|
||||
| 36 | CMD_SEND_TRACE_PATH | Request path trace |
|
||||
| 38 | CMD_SET_OTHER_PARAMS | Set misc parameters |
|
||||
| 39 | CMD_GET_TELEMETRY_REQ | Request sensor telemetry |
|
||||
| 39 | CMD_SEND_TELEMETRY_REQ | Request sensor telemetry |
|
||||
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
|
||||
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
|
||||
| 50 | CMD_SEND_BINARY_REQ | Send binary request |
|
||||
| 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 |
|
||||
| 59 | CMD_GET_AUTO_ADD_CONFIG | Get auto-add configuration |
|
||||
| 61 | CMD_SET_PATH_HASH_MODE | Set path hash width (bytes per hop) |
|
||||
|
||||
## Response / Push Codes (Device → App)
|
||||
|
||||
@@ -145,6 +150,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
|
||||
| 17 | RESP_CODE_CHANNEL_MSG_RECV_V3 | Incoming channel message (v3) |
|
||||
| 18 | RESP_CODE_CHANNEL_INFO | Channel definition |
|
||||
| 21 | RESP_CODE_CUSTOM_VARS | Custom variables |
|
||||
| 24 | RESP_CODE_STATS | Companion radio stats |
|
||||
| 25 | RESP_CODE_AUTO_ADD_CONFIG | Auto-add flags |
|
||||
| 0x80 | PUSH_CODE_ADVERT | Known contact re-seen |
|
||||
| 0x81 | PUSH_CODE_PATH_UPDATED | Better path found; carries the 32-byte public key of the updated contact |
|
||||
@@ -158,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
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
Channels are broadcast group-chat spaces secured by a 16-byte pre-shared key (PSK). Any device with the same channel index and PSK will receive and decrypt channel messages. Unlike direct messages, channel messages are broadcast to the entire mesh.
|
||||
|
||||
Up to 8 channels (indices 0–7) can be active simultaneously on one device.
|
||||
The number of active channels is determined by the firmware (default 40); the device reports its actual limit at login.
|
||||
|
||||
## How to Access
|
||||
|
||||
@@ -17,7 +17,7 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
||||
| Public | Globe | Green | Fixed well-known PSK; any device can join |
|
||||
| Hashtag | Hash tag | Blue | PSK derived from the hashtag name via SHA-256; discoverable by convention |
|
||||
| Private | Lock | Blue | Random PSK; requires out-of-band sharing of the 32-hex key |
|
||||
| Community | Groups/Tag | Purple | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||
| Community | Groups/Tag | Magenta | PSK derived via HMAC-SHA256 from a community's shared secret |
|
||||
|
||||
## Channels List Screen
|
||||
|
||||
@@ -26,12 +26,12 @@ QuickSwitchBar tab 1 (middle) from any main screen.
|
||||
- **Search bar** with live text filtering (300ms debounce)
|
||||
- **Sort/filter button**
|
||||
- **Scrollable list of channel cards**, each showing:
|
||||
- Type icon with color coding (purple badge overlay for community channels)
|
||||
- Type icon with color coding (magenta badge overlay for community channels)
|
||||
- Channel name (or "Channel N" if unnamed)
|
||||
- Unread badge (if messages are unread)
|
||||
- Drag handle (when manual sort is active)
|
||||
- **"+" FAB** to add a new channel
|
||||
- **Overflow menu**: Disconnect, Manage Communities (only shown when at least one community exists), Settings
|
||||
- **Overflow menu**: Disconnect, Manage Communities, Settings
|
||||
|
||||
If no channels exist, an empty state with an "Add Public Channel" shortcut is shown. If a search produces no results, a separate "no results" empty state with a search-off icon is shown.
|
||||
|
||||
@@ -59,7 +59,7 @@ Tap the "+" FAB to open a dialog with six options:
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| Edit | Change name, PSK (with a dice icon to generate a random PSK), or SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit) |
|
||||
| Edit | Change name, PSK (with a dice icon to generate a random PSK), SMAZ compression toggle (compresses outgoing messages to allow longer text within the byte limit), or Cyr2Lat encoding toggle (transliterates Cyrillic to Latin for compatibility) |
|
||||
| Mute / Unmute | Toggle push notification suppression for this channel |
|
||||
| Delete | Remove the channel from the device (confirmation required) |
|
||||
|
||||
@@ -100,8 +100,7 @@ Tap a channel card to open the channel chat screen.
|
||||
|
||||
### Message Path Viewing
|
||||
|
||||
- **Mobile**: Tap a message bubble to view its routing path
|
||||
- **Desktop**: Long-press/right-click → "Path" (tapping the bubble does nothing on desktop)
|
||||
- **All platforms**: Long-press (or right-click on desktop) a message bubble → "Path"
|
||||
- Opens the Channel Message Path Screen (see [Additional Features](additional-features.md))
|
||||
|
||||
### Context Actions (Long-Press / Right-Click)
|
||||
@@ -109,7 +108,7 @@ Tap a channel card to open the channel chat screen.
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Reply | All messages | Triggers reply mode |
|
||||
| Path | Desktop only | Opens message path view |
|
||||
| Path | All messages | Opens message path view |
|
||||
| Add Reaction | Incoming messages only | Opens emoji picker (cannot react to your own messages) |
|
||||
| Copy | All messages | Copies text to clipboard |
|
||||
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||
@@ -142,7 +141,7 @@ From the channels screen overflow menu → "Manage Communities". Opens a draggab
|
||||
- **Tap a community** to directly show its QR code for sharing
|
||||
- **Popup menu** per community:
|
||||
- **Show QR** — displays the QR code for sharing with new members
|
||||
- **Delete** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
|
||||
- **Leave Community** — removes the community locally and deletes all associated device channels (confirmation dialog warns how many channels will be removed)
|
||||
|
||||
## How Channels Differ from Direct Messages
|
||||
|
||||
|
||||
@@ -18,17 +18,15 @@ From the Contacts screen, tap any Chat-type contact to open the ChatScreen.
|
||||
|
||||
- **Title**: Contact name
|
||||
- **Subtitle**: Current routing path label (e.g., "2 hops", "flood (auto)", "direct (forced)") and unread count. Tapping the subtitle shows the full path details.
|
||||
- **Action buttons**:
|
||||
- **Routing mode** (waves icon): Switch between Auto, Direct, and Flood routing
|
||||
- **Path management** (timeline icon): View recent paths with hop count, round-trip time, age, and success count. Paths are color-coded by direct repeater (green/yellow/red/blue for ranked repeaters, grey for unknown). Tap a path to activate it (the device verifies and confirms via snackbar), long-press to view full path details, set custom paths, or force flood mode. A warning banner appears when history reaches 100 entries.
|
||||
- **Info** (info icon): Contact info dialog showing type, path, GPS coordinates, public key, and SMAZ compression toggle
|
||||
- **Action button**:
|
||||
- **Overflow menu** (⋮ icon): Contains Routing, Info, Telemetry, Settings, and Clear Chat. Routing opens the routing sheet where you can switch between Auto, Direct, and Flood routing and manage recent paths (hop count, round-trip time, age, success count, color-coded by repeater). Info shows a dialog with contact type, path, GPS coordinates, and public key.
|
||||
|
||||
### Message List
|
||||
|
||||
- Scrollable list with newest messages at the bottom
|
||||
- **Outgoing messages**: Right-aligned, primary color background. **Failed messages** change to a red-toned error container background
|
||||
- **Incoming messages**: Left-aligned, grey background with a colored avatar (initial letter or first emoji of sender name; color is deterministic from a hash of the sender name)
|
||||
- Bubble width capped at 65% of screen width
|
||||
- Bubble width capped at 72% of screen width
|
||||
- Hyperlinks rendered as tappable green underlined text
|
||||
- **Pinch-to-zoom**: Two-finger zoom (0.8x–1.8x) and double-tap to reset
|
||||
- **Jump to bottom**: Floating button appears when scrolled away from the bottom
|
||||
@@ -87,7 +85,7 @@ When a direct message is sent:
|
||||
|
||||
1. The app computes an expected ACK hash: `SHA256([timestamp][attempt][text][selfPubKey])[0:4]` — matching the firmware's hash calculation. If SMAZ compression is enabled, the compressed text (not the original) is hashed
|
||||
2. On device acknowledgment (`RESP_CODE_SENT`), the message transitions to "sent" and a timeout timer starts
|
||||
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate)
|
||||
3. **Timeout duration**: Preferably from the ML timeout prediction service; otherwise from the device's own `est_timeout` in `RESP_CODE_SENT` (clamped to the physics range); otherwise calculated from LoRa airtime physics: `500 + (airtime × 6 + 250) × (pathLength + 1)` ms for direct paths, `500 + 16 × airtime` ms for flood (airtime is estimated from the radio's current spreading factor, bandwidth, and coding rate). The result is capped at 45 seconds.
|
||||
4. On timeout, the message is retried with **exponential backoff**: `1000 × 2^retryCount` ms (1s, 2s, 4s, 8s, 16s...)
|
||||
5. **Max retries**: Configurable (default 5, range 2–10)
|
||||
6. After max retries, the message is marked "failed" — but a **30-second grace window** remains during which a late ACK can still resolve the message to "delivered"
|
||||
@@ -114,8 +112,9 @@ Add emoji reactions to incoming messages (not your own):
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Add reaction | Incoming messages only | Opens emoji picker |
|
||||
| View path | Mobile: tap bubble directly; Desktop: long-press/right-click menu | Shows message routing path |
|
||||
| View path | All platforms: long-press/right-click menu | Shows message routing path |
|
||||
| Copy | All messages | Copies text to clipboard |
|
||||
| Translate | Incoming messages only (when translation is enabled and not yet translated) | Translates the message on-demand using the on-device model |
|
||||
| Mark as Unread | Incoming messages only | Marks this message and all subsequent incoming messages as unread |
|
||||
| Delete | All messages | Removes locally (not from mesh) |
|
||||
| Retry | Failed outgoing messages | Re-sends the message |
|
||||
|
||||
@@ -6,18 +6,17 @@ The Contacts screen is the primary hub for managing mesh nodes your radio has a
|
||||
|
||||
## How to Access
|
||||
|
||||
- Automatically shown after connecting to a device
|
||||
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens
|
||||
- QuickSwitchBar tab 0 (leftmost) from Channels or Map screens (Channels is shown first after connecting)
|
||||
- Back navigation from Chat or Settings screens
|
||||
|
||||
## Contact Types
|
||||
|
||||
| Type | Avatar Color | Icon | Description |
|
||||
|---|---|---|---|
|
||||
| Chat | Blue | Chat bubble | Another user's mesh radio |
|
||||
| Repeater | Orange | Cell tower | A mesh repeater/relay node |
|
||||
| Room | Purple | Group | A room server for group chat |
|
||||
| Sensor | Green | Sensors | A sensor device |
|
||||
| Chat | Blue | Initials / emoji | Another user's mesh radio |
|
||||
| Repeater | Amber | Cell tower | A mesh repeater/relay node |
|
||||
| Room | Magenta | Meeting room | A room server for group chat |
|
||||
| Sensor | Teal | Sensors | A sensor device |
|
||||
|
||||
## Contact List
|
||||
|
||||
@@ -73,42 +72,42 @@ Groups are stored per radio identity (scoped by public key).
|
||||
|
||||
| Action | Availability | Description |
|
||||
|---|---|---|
|
||||
| Ping | Repeaters (always) | Opens PathTraceMapScreen targeting the repeater |
|
||||
| Path Trace | Rooms (always); Chat/Sensor if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
|
||||
| Ping | Repeaters only | Opens PathTraceMapScreen targeting the repeater |
|
||||
| Path Trace | Rooms (always); Chat/Sensor only if `pathLength > 0` | Opens PathTraceMapScreen. For rooms, label shows "Ping" when no path bytes are known, "Path Trace" when path bytes are available |
|
||||
| Manage Repeater | Repeaters only | Login dialog → RepeaterHubScreen |
|
||||
| Room Login | Rooms only | Login dialog → ChatScreen |
|
||||
| Room Management | Rooms only | Login dialog → RepeaterHubScreen (management mode) |
|
||||
| Open Chat | Chat/Sensor | Same as single tap |
|
||||
| Add/Remove Favorite | All types | Toggles the favorite flag |
|
||||
| Share Contact | All types | Copies `meshcore://<hex>` URI to clipboard |
|
||||
| Share Contact | All types | Requests advert from device → copies `meshcore://<hex>` URI to clipboard |
|
||||
| Share Contact Zero-Hop | All types | Broadcasts the contact's advertisement one hop |
|
||||
| Delete Contact | All types | Confirmation dialog → removes from device and clears messages |
|
||||
|
||||
## App Bar Menus
|
||||
|
||||
The Contacts screen has **two separate popup menus** in the app bar:
|
||||
The Contacts screen has a single **three-dot overflow menu** (`⋮`) in the app bar:
|
||||
|
||||
**Antenna icon menu** (contact sharing):
|
||||
- Discovered Contacts — opens the DiscoveryScreen
|
||||
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||
- *(divider)*
|
||||
- Zero-Hop Advert — broadcasts your advertisement to immediately adjacent nodes
|
||||
- Flood Advert — broadcasts across the full mesh network
|
||||
- Copy Advert to Clipboard — copies your `meshcore://<hex>` URI for sharing externally
|
||||
- Add Contact from Clipboard — reads a `meshcore://<hex>` URI from clipboard and imports it
|
||||
|
||||
**Three-dot overflow menu**:
|
||||
- *(divider)*
|
||||
- Disconnect — disconnects from the device
|
||||
- Discovered Contacts — opens the DiscoveryScreen
|
||||
- Settings — opens the Settings screen
|
||||
|
||||
A **floating action button** (person-add icon) provides a shortcut sheet to "Add Contact from Clipboard" or "Discovered Contacts".
|
||||
|
||||
## Adding Contacts
|
||||
|
||||
### Automatic (Passive)
|
||||
When the radio hears an advertisement, the contact appears automatically if auto-add is enabled for that type (configurable in Settings → Contact Settings).
|
||||
|
||||
### Import from Clipboard
|
||||
Antenna menu → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||
Overflow menu (or the FAB shortcut) → "Add Contact from Clipboard". Reads a `meshcore://<hex>` URI from clipboard and imports it to the device.
|
||||
|
||||
### Import from Discovered Contacts
|
||||
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Add, Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms, Favorites), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||
Overflow menu → "Discovered Contacts". Shows nodes heard passively that haven't been added yet. Tap to immediately import (no confirmation dialog), or long-press for more options (Copy URI, Delete). The Discovery screen has its own search bar, type filters (Users, Repeaters, Rooms), and sort options (Last Seen, A-Z). An overflow "Delete All" option clears all discovered contacts.
|
||||
|
||||
## Contact Sharing Format
|
||||
|
||||
|
||||
@@ -35,9 +35,9 @@ Location pins shared in chat messages are displayed as flags:
|
||||
|
||||
Tap a pin to see its info. Options to "Hide" (session only) or "Remove" (persistent).
|
||||
|
||||
### Predicted / Guessed Locations (Semi-Transparent)
|
||||
### Predicted / Guessed Locations
|
||||
|
||||
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as semi-transparent markers with a `not_listed_location` icon, visually distinct from confirmed-location markers.
|
||||
Many contacts on the mesh don't have GPS hardware, so the map has no explicit coordinates for them. Instead of leaving these contacts invisible, the app **infers an approximate position** by analyzing the repeater path the contact's messages travel through. These inferred positions are displayed as markers with a `not_listed_location` icon and a muted grey or colored border, visually distinct from confirmed-location markers.
|
||||
|
||||
#### Why guessed locations exist
|
||||
|
||||
@@ -55,19 +55,19 @@ In a mesh network, every message hops through one or more repeaters on its way t
|
||||
|
||||
5. **Compute the estimated position**:
|
||||
- **Single anchor**: The contact is placed on a small circle (330m radius) around the repeater. The angle on the circle is deterministic — derived from an FNV-1a hash of the contact's public key — so the same contact always appears at the same offset, preventing markers from stacking on top of each other.
|
||||
- **Two or more anchors**: The position is the average (centroid) of all anchor coordinates, with a smaller offset radius (80–120m) applied for visual separation.
|
||||
- **Two or more anchors**: The position is a weighted average of all anchor coordinates (each subsequent anchor weighted at half the previous one, biasing toward the first), with a smaller offset radius (120m for 2 anchors, 80m for 3+) applied for visual separation.
|
||||
|
||||
6. **Assign confidence level**:
|
||||
- **High confidence** (2+ anchors): Displayed at 55% opacity.
|
||||
- **Low confidence** (1 anchor): Displayed at 30% opacity.
|
||||
- **High confidence** (2+ anchors): The marker border uses the node's type color (brighter border).
|
||||
- **Low confidence** (1 anchor): The marker border is rendered in a muted grey.
|
||||
|
||||
7. **Cache the result**: The computation is cached using a key derived from the contact's paths, anchor positions, path-history version, and radio parameters. The cache is only invalidated when any of these inputs change, avoiding recomputation on every UI rebuild.
|
||||
|
||||
#### How to read guessed locations on the map
|
||||
|
||||
- **Semi-transparent marker** with a `not_listed_location` icon: This is a guessed position, not a confirmed GPS fix.
|
||||
- **More opaque** (55%): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||
- **More transparent** (30%): Lower confidence — based on a single repeater anchor only.
|
||||
- **Marker with `not_listed_location` icon**: This is a guessed position, not a confirmed GPS fix.
|
||||
- **Colored border** (type color): Higher confidence — the contact was seen through 2 or more repeaters with known positions.
|
||||
- **Grey border**: Lower confidence — based on a single repeater anchor only.
|
||||
- Coordinates shown in the marker info dialog are prefixed with `~` to indicate they are estimated.
|
||||
- Guessed locations can be toggled on/off in the map filter dialog (FAB → "Guessed locations" toggle).
|
||||
|
||||
@@ -110,9 +110,16 @@ A map with a polyline showing the route from your node through repeater hops to
|
||||
- **Green circles**: Hops with known GPS coordinates
|
||||
- **Orange circles** (`~HH`): Inferred positions (no GPS but deducible from contacts)
|
||||
- **Red endpoint**: Target contact with known GPS
|
||||
- **Purple semi-transparent endpoint**: Target with guessed position
|
||||
- **Magenta endpoint**: Target with guessed position
|
||||
|
||||
A legend card at the bottom lists each hop pair with SNR quality icons and total path distance.
|
||||
A bottom panel shows each hop pair with SNR quality icons and total path distance. When multiple observed paths are available, a **Single / Combined** toggle appears at the top of the map. In Combined view, all paths are overlaid; shared segments are highlighted with a white halo and a path count badge appears on shared nodes.
|
||||
|
||||
The bottom panel also provides **packet animation controls**:
|
||||
- **Animation toggle** (on/off)
|
||||
- **Step back / Play / Step forward / Replay** buttons
|
||||
- **Follow packet lock** — keeps the map camera centered on the moving packet dot
|
||||
- **Speed selector** (0.5×, 1×, 2×, 4×)
|
||||
- A live **"Hop x of y · from → to"** label that tracks the active segment
|
||||
|
||||
### How It Works
|
||||
Sends a trace request frame over the mesh. The repeater network traces the path hop-by-hop and returns per-hop SNR data. For hops without GPS, positions are inferred by averaging GPS coordinates of contacts sharing that last-hop byte.
|
||||
@@ -125,17 +132,17 @@ Sends a trace request frame over the mesh. The repeater network traces the path
|
||||
From the main map, tap the terrain/antenna icon.
|
||||
|
||||
### What the User Sees
|
||||
A full-screen map with a collapsible control panel containing:
|
||||
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow)
|
||||
- **Status**: Clear (green) or blocked (red) with distance and minimum clearance
|
||||
- **Options panel**: Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||
A full-screen map with a draggable bottom sheet containing:
|
||||
- **Elevation profile chart**: Terrain fill (green), LOS beam line (white), radio horizon line (yellow); obstruction points are marked as clickable dots on the chart
|
||||
- **Status summary**: Clear (green), Marginal (amber, within 5 m of obstruction), or Blocked (red) with distance and clearance/obstruction amount
|
||||
- **Options section** (collapsible): Node toggles, endpoint dropdowns, antenna height sliders (0–400 ft), Run LOS button
|
||||
|
||||
### Key Interactions
|
||||
- **Long-press the map** to add custom endpoints (orange pushpin markers, renameable/deleteable)
|
||||
- **Long-press the map** to add custom endpoints (pushpin markers, renameable/deleteable)
|
||||
- **Tap a marker** to select it as Point A or B; LOS runs automatically when both are set
|
||||
- **Antenna heights** are adjustable for both endpoints
|
||||
- **Map line** between endpoints is colored green (clear) or red (blocked)
|
||||
- Terrain elevation is fetched from the Open-Meteo API (21–81 sample points, cached 24 hours)
|
||||
- **Map line** between endpoints is colored green (clear), amber (marginal), or red (blocked)
|
||||
- Terrain elevation is fetched from the Open-Meteo API (21, 41, or 81 sample points depending on link distance, cached 24 hours)
|
||||
- K-factor is adjusted per radio frequency from a baseline of 4/3 at 915 MHz
|
||||
|
||||
---
|
||||
@@ -149,7 +156,7 @@ Settings → App Settings → Map Display → Offline Map Cache
|
||||
- Map with a blue polygon overlay showing previously selected cache bounds
|
||||
- Bounding box coordinates card
|
||||
- **Cache Area** controls: "Use Current View" and Clear buttons
|
||||
- **Zoom Range** slider (3–18) with estimated tile count
|
||||
- **Zoom Range** range slider (3–18, dual-handle for min and max) with estimated tile count
|
||||
- **Download progress** bar (when downloading)
|
||||
- **Download Tiles** and **Clear Cache** buttons
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
The app follows this general flow:
|
||||
|
||||
```
|
||||
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Contacts Screen
|
||||
Launch → Scanner Screen → [Connect via BLE/USB/TCP] → Channels Screen
|
||||
```
|
||||
|
||||
After connecting, the three main screens (Contacts, Channels, Map) are accessible via a persistent bottom navigation bar called the **QuickSwitchBar**.
|
||||
@@ -24,7 +24,7 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal
|
||||
|
||||
## Disconnection
|
||||
|
||||
- The disconnect button (available in the Settings screen and other main screens) shows a confirmation dialog before disconnecting
|
||||
- The disconnect button (available in the overflow menu of each main screen) shows a confirmation dialog before disconnecting
|
||||
- If the device disconnects unexpectedly, the app automatically navigates back to the Scanner screen (fires after the current frame completes via a post-frame callback)
|
||||
- This auto-navigation behavior (`DisconnectNavigationMixin`) is shared across all main screens
|
||||
|
||||
@@ -38,11 +38,11 @@ Tapping a tab replaces the current screen with a subtle fade + slight horizontal
|
||||
|
||||
```
|
||||
ScannerScreen (root, always on stack)
|
||||
├─ [BLE connect] → push → ContactsScreen
|
||||
├─ [TCP FAB] → push → TcpScreen
|
||||
│ └─ [TCP connected] → pushReplacement → ContactsScreen
|
||||
└─ [USB FAB] → push → UsbScreen
|
||||
└─ [USB connected] → pushReplacement → ContactsScreen
|
||||
├─ [BLE connect] → push → ChannelsScreen
|
||||
├─ [TCP icon button] → push → TcpScreen
|
||||
│ └─ [TCP connected] → pushReplacement → ChannelsScreen
|
||||
└─ [USB icon button] → push → UsbScreen
|
||||
└─ [USB connected] → pushReplacement → ChannelsScreen
|
||||
|
||||
ContactsScreen (selected=0)
|
||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||
@@ -60,9 +60,9 @@ ChannelsScreen (selected=1)
|
||||
MapScreen (selected=2)
|
||||
├─ [quick-switch 0] → pushReplacement → ContactsScreen
|
||||
├─ [quick-switch 1] → pushReplacement → ChannelsScreen
|
||||
├─ [radar button] → push → PathTraceMapScreen
|
||||
├─ [terrain button] → push → LineOfSightMapScreen
|
||||
└─ [long-press] → share marker / set location
|
||||
├─ [radar menu item] → enters in-map path trace mode (push → PathTraceMapScreen after path is built)
|
||||
├─ [terrain menu item] → push → LineOfSightMapScreen
|
||||
└─ [long-press] → share marker sheet
|
||||
|
||||
Settings (push from any main screen)
|
||||
└─ [App Settings] → push → AppSettingsScreen
|
||||
|
||||
@@ -22,7 +22,7 @@ MeshCore Open provides both **system notifications** (push-style OS alerts) and
|
||||
|
||||
### 3. Advertisement Notifications
|
||||
- **Triggered when**: A new node is discovered on the mesh for the first time
|
||||
- **Title**: "New [type] discovered" (e.g., "New chat node discovered")
|
||||
- **Title**: "New [type] discovered" (e.g., "New Chat discovered")
|
||||
- **Body**: Contact's name
|
||||
- **Priority**: Default
|
||||
- **Android channel**: `adverts`
|
||||
@@ -43,7 +43,7 @@ Red numeric badges appear throughout the UI:
|
||||
- **Contacts list**: Each contact row shows a red pill badge (e.g., "3") for unread messages
|
||||
- **Channels list**: Each channel row shows an unread badge
|
||||
- **Chat screen subtitle**: Shows unread count inline
|
||||
- Badges cap at "99+" for display
|
||||
- Badges cap at "9999+" for display
|
||||
|
||||
### How Unread Counts Work
|
||||
|
||||
|
||||
@@ -34,8 +34,8 @@ The central management screen showing:
|
||||
|---|---|---|
|
||||
| Status | Repeater Status Screen | All users |
|
||||
| Telemetry | Telemetry Screen | All users |
|
||||
| CLI | Repeater CLI Screen | Admin only |
|
||||
| Neighbors | Neighbors Screen | All users |
|
||||
| CLI | Repeater CLI Screen | Admin only |
|
||||
| Settings | Repeater Settings Screen | Admin only |
|
||||
|
||||
The battery chemistry selector and CLI/Settings cards are hidden from guest users.
|
||||
@@ -89,7 +89,7 @@ A terminal-style interface for sending commands directly to the repeater.
|
||||
- Type a command and press send (or Enter on desktop)
|
||||
- Up/down arrows navigate through command history
|
||||
- Quick-command buttons populate and send common commands
|
||||
- Bug report icon: Shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||
- Overflow menu (three-dot icon): "Debug next command" option shows raw frame debug info for the next typed command (shows error snackbar if input field is empty)
|
||||
- Help icon: Opens a scrollable reference of all known CLI commands. Tapping any command populates the input field immediately
|
||||
- Clear icon: Wipes the command/response history
|
||||
- Failed/timed-out commands are automatically retried once
|
||||
@@ -112,7 +112,9 @@ The in-app help reference (help icon) documents all known commands. Categories:
|
||||
|
||||
**Power Management**: `get pwrmgt.support`, `get pwrmgt.source`, `get pwrmgt.bootreason`, `get pwrmgt.bootmv`
|
||||
|
||||
**Sensors**: `sensor get {key}`
|
||||
**Sensors**: `sensor get {key}`, `sensor set {key} {value}`, `sensor list [start]`
|
||||
|
||||
**GPS Management**: `gps`, `gps {on|off}`, `gps sync`, `gps setloc`, `gps advert`, `gps advert {none|share|prefs}`
|
||||
|
||||
**Region Management**: `region`, `region load`, `region get`, `region put`, `region remove`, `region allowf`, `region denyf`, `region home`, `region save`, `region default`, `region list allowed`, `region list denied`
|
||||
|
||||
@@ -207,7 +209,7 @@ Nine configuration cards, each with its own per-field refresh button(s):
|
||||
|
||||
**Danger Zone** (red-styled card)
|
||||
- Reboot repeater (sends `reboot` with confirmation dialog)
|
||||
- Erase filesystem (serial-only; shows informational snackbar only — no command is sent over the air)
|
||||
- Erase filesystem (serial-only; shows a confirmation dialog, then an informational snackbar — no command is sent over the air)
|
||||
|
||||
### Key Interactions
|
||||
- **Settings are NOT auto-fetched on open**. Name is pre-filled from cached contact data. Each section has its own refresh button to fetch live values from the repeater
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -28,9 +28,11 @@ The BLE Scanner is the app's home screen, displayed immediately on launch.
|
||||
|
||||
**Device List**: When no devices are found, shows a large Bluetooth icon with a prompt. The prompt text is dynamic: "Searching for devices..." while actively scanning, or "Tap Scan to search" when idle. When devices are found, shows a scrollable list of `DeviceTile` widgets.
|
||||
|
||||
**Bottom FAB Row**: Up to three floating action buttons:
|
||||
- **USB** button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||
- **TCP/IP** button - Opens TCP connection screen (all non-web platforms)
|
||||
**App Bar Actions**: Icon buttons in the top-right corner of the app bar:
|
||||
- **USB** icon button - Opens USB connection screen (Android, Windows, Linux, macOS, Chrome web only)
|
||||
- **TCP/IP** icon button - Opens TCP connection screen (all non-web platforms)
|
||||
|
||||
**Bottom FAB**: A single floating action button:
|
||||
- **BLE Scan** button - Toggles BLE scanning on/off; shows a spinner when scanning. **Disabled** (greyed out, not tappable) when Bluetooth is off
|
||||
|
||||
### Device Tile
|
||||
@@ -51,7 +53,7 @@ Note: The weak (-80 to -90 dBm) and poor (< -90 dBm) tiers share the same icon s
|
||||
|
||||
### How Scanning Works
|
||||
|
||||
- Filters for devices with names starting with one of the known prefixes: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`
|
||||
- Filters for devices advertising the Nordic UART Service UUID (so community forks with non-standard names are still found). Known name prefixes used by stock firmware builds for reference: `MeshCore-`, `Whisper-`, `WisCore-`, `Seeed`, `Lilygo`, `HT-`, `LowMesh_MC_`, `NRF52`
|
||||
- Uses low-latency scan mode on Android
|
||||
- Scans for 10 seconds then auto-stops
|
||||
- On iOS/macOS, waits for BLE adapter initialization before starting
|
||||
@@ -65,7 +67,7 @@ Tap a device tile or its Connect button:
|
||||
3. Requests MTU 185 bytes for optimal throughput
|
||||
4. Discovers BLE services and locates the Nordic UART Service
|
||||
5. Subscribes to TX notifications for receiving data
|
||||
6. On success, automatically navigates to the Contacts screen
|
||||
6. On success, automatically navigates to the Channels screen
|
||||
7. On failure, shows a red error snackbar
|
||||
|
||||
---
|
||||
@@ -74,7 +76,7 @@ Tap a device tile or its Connect button:
|
||||
|
||||
### How to Access
|
||||
|
||||
From the Scanner screen, tap the **USB** FAB button.
|
||||
From the Scanner screen, tap the **USB** icon button in the app bar.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
@@ -82,15 +84,15 @@ From the Scanner screen, tap the **USB** FAB button.
|
||||
- A list of detected USB serial ports, each showing:
|
||||
- Friendly display name
|
||||
- Raw port name (subtitle, only shown when it differs from the display name)
|
||||
- "Connect" button
|
||||
- FABs at the bottom to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||
- Chevron trailing icon (the entire tile is tappable to connect)
|
||||
- Transport switcher buttons (outlined, not FABs) to switch to BLE or TCP (these use `pushReplacement`, so back navigation returns to Scanner, not between USB/TCP)
|
||||
|
||||
### Key Interactions
|
||||
|
||||
- On desktop (Windows, Linux, macOS): ports are polled every 2 seconds for hot-plug detection (polling pauses while connecting/connected)
|
||||
- On mobile: tap the "Scan" FAB to manually refresh
|
||||
- Tap a port or its Connect button to connect
|
||||
- On successful connection, navigates to Contacts screen
|
||||
- Tap a port tile to connect
|
||||
- On successful connection, navigates to Channels screen
|
||||
- On connection failure, the port list automatically refreshes
|
||||
- Platform-specific error messages for common USB failures (permission denied, device missing, device detached, device busy, driver missing, port invalid, timeout, and more)
|
||||
|
||||
@@ -100,7 +102,7 @@ From the Scanner screen, tap the **USB** FAB button.
|
||||
|
||||
### How to Access
|
||||
|
||||
From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
From the Scanner screen, tap the **TCP/IP** icon button in the app bar.
|
||||
|
||||
### What the User Sees
|
||||
|
||||
@@ -108,7 +110,7 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
- **Host address** text field
|
||||
- **Port number** text field
|
||||
- **Connect** button
|
||||
- FABs at the bottom to switch to USB or BLE
|
||||
- Transport switcher buttons (outlined, not FABs) to switch to USB or BLE
|
||||
|
||||
### Key Interactions
|
||||
|
||||
@@ -119,6 +121,6 @@ From the Scanner screen, tap the **TCP/IP** FAB button.
|
||||
- Validation errors are shown as red snackbars
|
||||
- The Connect button shows a spinner and "Connecting..." label while in progress
|
||||
- The status bar shows the specific host:port being connected to (e.g., "Connecting to 192.168.1.1:5000")
|
||||
- On success, navigates to Contacts screen and saves the host/port to settings
|
||||
- On success, navigates to Channels screen and saves the host/port to settings
|
||||
- On connection, the status bar shows the active TCP endpoint (e.g., "Connected to 192.168.1.1:5000")
|
||||
- Error messages for timeout, unsupported platform, and connection failures
|
||||
|
||||
@@ -12,12 +12,13 @@ Settings are only accessible while a device is connected.
|
||||
The settings screen is a scrollable list of cards:
|
||||
|
||||
1. [Device Info](#device-info)
|
||||
2. [App Settings](#app-settings) (link to sub-screen)
|
||||
3. [Node Settings](#node-settings)
|
||||
4. [Actions](#actions)
|
||||
5. [Debug](#debug)
|
||||
2. [Node Settings](#node-settings)
|
||||
3. [Location](#location)
|
||||
4. [App Settings](#app-settings) (link to sub-screen)
|
||||
5. [Actions](#actions)
|
||||
6. [Export](#export)
|
||||
7. [About](#about)
|
||||
7. [Debug](#debug)
|
||||
8. [About](#about)
|
||||
|
||||
---
|
||||
|
||||
@@ -40,56 +41,6 @@ Battery shows an alert icon and orange text when at 15% or below. The toggle onl
|
||||
|
||||
---
|
||||
|
||||
## App Settings
|
||||
|
||||
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||
|
||||
### Appearance
|
||||
- **Theme**: System / Light / Dark
|
||||
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
|
||||
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||
|
||||
### Notifications
|
||||
- **Master enable/disable**: Requests OS permission when enabling
|
||||
- **Message notifications**: New direct message alerts
|
||||
- **Channel message notifications**: New channel message alerts
|
||||
- **Advertisement notifications**: New node discovery alerts
|
||||
|
||||
### Messaging
|
||||
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
|
||||
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||
- Max Route Weight (1–10, default 5, integer steps)
|
||||
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||
- Max Message Retries (2–10, default 5)
|
||||
|
||||
### Battery
|
||||
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||
|
||||
### Translation
|
||||
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
|
||||
- **Enable Translation**: Translates incoming messages into the selected target language
|
||||
- **Translate Composer**: Translates outgoing messages from the target language back before sending
|
||||
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
|
||||
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
|
||||
- **Preset Model**: Download a curated preset model with one tap
|
||||
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
|
||||
|
||||
### Map Display
|
||||
- **Show Repeaters**: Toggle repeater markers on map
|
||||
- **Show Chat Nodes**: Toggle chat node markers
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **Offline Map Cache**: Navigate to tile download screen
|
||||
|
||||
### Debug
|
||||
- **App Debug Logging**: Enable the in-app debug log
|
||||
|
||||
---
|
||||
|
||||
## Node Settings
|
||||
|
||||
These settings are sent directly to the connected device firmware.
|
||||
@@ -101,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**: 19 regional presets — selecting a preset immediately fills all fields below. Full list: Australia, Australia (Narrow), Australia SA, WA, QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, Switzerland, USA Arizona, USA/Canada, Vietnam, Off-Grid 433, Off-Grid 869, Off-Grid 918
|
||||
- **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
|
||||
@@ -109,6 +60,13 @@ Opens a dialog pre-populated with the device's current radio settings. Contains:
|
||||
- **TX Power** (dBm): Validated 0 to device max (typically 22 dBm)
|
||||
- **Client Repeat** toggle: Only shown on firmware v9+; requires frequency to be exactly 433.000, 869.000, or 918.000 MHz (the Off-Grid presets). Save is blocked with a warning if enabled on other frequencies
|
||||
|
||||
### Companion Radio Stats
|
||||
Opens the RF statistics screen (RSSI, SNR, packet counts) for the paired radio. Only enabled when connected to a device that supports companion radio stats.
|
||||
|
||||
---
|
||||
|
||||
## Location
|
||||
|
||||
### Location
|
||||
Opens a dialog pre-populated with the device's current coordinates (if known):
|
||||
- Latitude and longitude fields (decimal, 6 decimal places). If only one field is provided, the other uses the device's current value
|
||||
@@ -125,8 +83,72 @@ Five toggles controlling which node types are auto-added when heard:
|
||||
- Auto-add Sensors
|
||||
- Overwrite Oldest (when contact list is full)
|
||||
|
||||
### Privacy Mode
|
||||
Opens a confirmation dialog with three buttons: Cancel, Enable, and Disable. Both states can be set from the same dialog regardless of current state. A snackbar confirms which state was applied. When on, the node stops broadcasting its location in advertisements.
|
||||
### Privacy
|
||||
Opens a dialog with controls for how the node shares telemetry and location data:
|
||||
- **Advert Location**: Toggle whether the node broadcasts its location in advertisements
|
||||
- **Multi-Ack**: Toggle multi-ack delivery confirmations
|
||||
- **Telemetry Base Mode**: Deny All / Allow by Contact / Allow All
|
||||
- **Telemetry Location Mode**: Deny All / Allow by Contact / Allow All
|
||||
- **Telemetry Environment Mode**: Deny All / Allow by Contact / Allow All
|
||||
|
||||
Settings take effect when saved. A snackbar confirms the update.
|
||||
|
||||
---
|
||||
|
||||
## App Settings
|
||||
|
||||
A dedicated sub-screen for app-level preferences (nothing here is sent to the device). All settings persist locally via SharedPreferences.
|
||||
|
||||
### Appearance
|
||||
- **Theme**: System / Light / Dark
|
||||
- **Language**: System default or one of 18 languages (English, French, Spanish, German, Polish, Slovenian, Portuguese, Italian, Chinese, Swedish, Dutch, Slovak, Bulgarian, Russian, Ukrainian, Hungarian, Japanese, Korean)
|
||||
|
||||
### Notifications
|
||||
- **Master enable/disable**: Requests OS permission when enabling
|
||||
- **Message notifications**: New direct message alerts
|
||||
- **Channel message notifications**: New channel message alerts
|
||||
- **Advertisement notifications**: New node discovery alerts
|
||||
|
||||
### Messaging
|
||||
- **Clear Path on Max Retry**: Erases the stored routing path after all retries fail
|
||||
- **Jump to Oldest Unread**: When opening a chat, scrolls to the oldest unread message instead of the newest
|
||||
- **Auto Route Rotation**: Enables weighted routing algorithm. When enabled, expands to show five slider sub-settings (hidden when off):
|
||||
- Max Route Weight (1–10, default 5, integer steps)
|
||||
- Initial Route Weight (0.5–5.0, default 3.0)
|
||||
- Success Increment (0.1–2.0, default 0.5, 0.1 steps)
|
||||
- Failure Decrement (0.1–2.0, default 0.2, 0.1 steps)
|
||||
- Max Message Retries (2–10, default 5)
|
||||
- **Enable Message Tracing**: Shows path trace overlays and extra metadata on messages
|
||||
|
||||
### Battery
|
||||
- **Battery Chemistry**: NMC / LiFePO4 / LiPo (per device, used to calibrate percentage from voltage)
|
||||
|
||||
### Map Display
|
||||
- **Show Repeaters**: Toggle repeater markers on map
|
||||
- **Show Chat Nodes**: Toggle chat node markers
|
||||
- **Show Other Nodes**: Toggle room/sensor markers
|
||||
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
|
||||
- **Units**: Metric / Imperial
|
||||
- **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
|
||||
Not shown on web. Controls on-device message translation powered by a locally-downloaded ML model:
|
||||
- **Enable Translation**: Translates incoming messages into the selected target language
|
||||
- **Translate Composer**: Translates outgoing messages from the target language back before sending
|
||||
- **Target Language**: Language to translate into (searchable list; defaults to the app language)
|
||||
- **Downloaded Model**: Dropdown to select among already-downloaded translation models
|
||||
- **Preset Model**: Download a curated preset model with one tap
|
||||
- **Custom Model URL**: Enter a URL to download a custom GGUF-format model; shows download progress and a cancel button
|
||||
|
||||
### Cyrillic-to-Latin (Cyr2Lat)
|
||||
Controls character substitution profiles used to render Cyrillic text in Latin characters. A dropdown selects the active profile; Add, Edit, and Delete buttons manage the profile list (the last remaining profile cannot be deleted). Each profile stores a JSON character map.
|
||||
|
||||
### Debug
|
||||
- **App Debug Logging**: Enable the in-app debug log
|
||||
|
||||
---
|
||||
|
||||
@@ -136,10 +158,24 @@ One-tap device operations:
|
||||
|
||||
| Action | Description |
|
||||
|---|---|
|
||||
| Send Advertisement | Floods the mesh with your node's advertisement |
|
||||
| Sync Time | Sends current Unix timestamp to the device |
|
||||
| Refresh Contacts | Re-requests the full contact list |
|
||||
| Reboot Device | Confirmation dialog → reboots the device (shown in orange) |
|
||||
| Reboot Device | Confirmation dialog → reboots the device (shown in warning color) |
|
||||
| Delete All Paths | Confirmation dialog → clears all stored routing paths (shown in alert color) |
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
Three GPX export options (not available on web):
|
||||
|
||||
| Option | Exports |
|
||||
|---|---|
|
||||
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||
| Export Contacts | Chat contacts with GPS coordinates |
|
||||
| Export All | All contacts with GPS coordinates |
|
||||
|
||||
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||
|
||||
---
|
||||
|
||||
@@ -160,20 +196,6 @@ Structured log entries (Info / Warning / Error), with tag, message, and timestam
|
||||
|
||||
---
|
||||
|
||||
## Export
|
||||
|
||||
Three GPX export options (not available on web):
|
||||
|
||||
| Option | Exports |
|
||||
|---|---|
|
||||
| Export Repeaters | Repeaters and Rooms with GPS coordinates |
|
||||
| Export Contacts | Chat contacts with GPS coordinates |
|
||||
| Export All | All contacts with GPS coordinates |
|
||||
|
||||
Each creates a `.gpx` file and opens the OS share sheet. Feedback via snackbar for four outcomes: success, no contacts with coordinates, feature not available (web), or error.
|
||||
|
||||
---
|
||||
|
||||
## About
|
||||
|
||||
Shows the standard Flutter about dialog with app name, version, and legal notice.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Bluetooth is niet beschikbaar in de browser. Verbind dan via USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snelle overschakeling';
|
||||
@@ -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';
|
||||
|
||||
@@ -1726,10 +1841,10 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get map_title => 'Kaart van de knopen';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Zoek op naam of ID van de knoop';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Activiteit';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
@@ -1738,25 +1853,25 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get map_recent => 'Recent';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Verouderd';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Zichtbaar';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Verborgen';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Centreer op node';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Geen GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Geen overeenkomende nodes';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Zichtlijn';
|
||||
@@ -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';
|
||||
|
||||
@@ -4502,48 +4725,48 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Afgeleide positie';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Enkel';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Gezamenlijk';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Afspelen';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Pauze';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Herhalen';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Vorige hop';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Volgende hop';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Pakketanimatie tonen';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Pakketanimatie verbergen';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Hop $current van $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Waargenomen paden: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Primair';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Alternatief $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4563,14 +4786,14 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Gedeeld segment';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Geschat segment';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Gebruikt door $count paden';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4578,33 +4801,232 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other:
|
||||
'$count hops hebben geen locatie — het weergegeven pad is onvolledig',
|
||||
one: '1 hop heeft geen locatie — het weergegeven pad is onvolledig',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Toon alles';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Verberg pad';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Toon pad';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Paneel inklappen';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Paneel uitklappen';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Geen locatie';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Weergave vergrendelen op pakket';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -324,7 +324,7 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Bluetooth nie jest dostępny w przeglądarce. Połącz się przez USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Szybka zmiana';
|
||||
@@ -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';
|
||||
|
||||
@@ -1755,37 +1870,37 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get map_title => 'Mapa węzłów';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Wyszukaj nazwę lub identyfikator węzła';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Aktywność';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Ostatnie';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Nieaktualne';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Widoczny';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Ukryty';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Wyśrodkuj na węźle';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Szczegóły';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Brak GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Brak pasujących węzłów';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linia wzroku';
|
||||
@@ -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';
|
||||
|
||||
@@ -4540,48 +4765,48 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Wywnioskowana pozycja';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Pojedyncza';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Połączone';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Odtwórz';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Wstrzymaj';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Odtwórz ponownie';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Poprzedni skok';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Następny skok';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Pokaż animację pakietu';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Ukryj animację pakietu';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Skok $current z $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Obserwowane trasy: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Główna';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Alt. $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4589,8 +4814,10 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count skoku',
|
||||
many: '$count skoków',
|
||||
few: '$count skoki',
|
||||
one: '1 skok',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4601,14 +4828,14 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Wspólny segment';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Szacunkowy segment';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Wykorzystywane przez $count ścieżek';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4616,33 +4843,235 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other:
|
||||
'$count skoku nie ma lokalizacji — pokazana ścieżka jest niekompletna',
|
||||
many:
|
||||
'$count skoków nie ma lokalizacji — pokazana ścieżka jest niekompletna',
|
||||
few:
|
||||
'$count skoki nie mają lokalizacji — pokazana ścieżka jest niekompletna',
|
||||
one: '1 skok nie ma lokalizacji — pokazana ścieżka jest niekompletna',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Pokaż wszystkie';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Ukryj ścieżkę';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Wyświetl trasę';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Zwiń panel';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Rozwiń panel';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Brak lokalizacji';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Śledź pakiet';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Mudar rapidamente';
|
||||
@@ -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';
|
||||
|
||||
@@ -1739,37 +1853,37 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get map_title => 'Mapa de Nós';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Pesquisar por nome ou ID do nó';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Atividade';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Recente';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Vencido';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Visível';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Escondido';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Centralizar no nó';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Detalhes';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Sem GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Nenhum nó encontrado';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linha de visão';
|
||||
@@ -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';
|
||||
|
||||
@@ -4519,44 +4743,44 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Posição inferida';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Único';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Combinado';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Reproduzir';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Pausa';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Repetir';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Salto anterior';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Próximo salto';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Exibir animação do pacote';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Ocultar a animação do pacote';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Salto $current de $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Caminhos observados: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Primário';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
@@ -4568,8 +4792,8 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count saltos',
|
||||
one: '1 salto',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4580,14 +4804,14 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Segmento compartilhado';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Segmento estimado';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Utilizado em $count caminhos';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4595,33 +4819,231 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count saltos não têm localização — o caminho mostrado é parcial',
|
||||
one: '1 salto não tem localização — o caminho mostrado é parcial',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Mostrar tudo';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Esconder caminho';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Mostrar o caminho';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Recolher painel';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Expandir painel';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Sem localização';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Fixar vista no pacote';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,7 +322,7 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Bluetooth недоступен в браузере. Подключитесь через USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Быстрое переключение';
|
||||
@@ -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 => 'Ответить';
|
||||
|
||||
@@ -1744,37 +1861,37 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get map_title => 'Карта нод';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Поиск по имени или ID узла';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Активность';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
String get map_online => 'Онлайн';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Недавно';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Устаревший';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Видимый';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Скрытый';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Центрировать на узле';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Детали';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Без GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Не найдено соответствующих узлов';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Линия видимости';
|
||||
@@ -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 => 'Включить перевод';
|
||||
|
||||
@@ -4535,48 +4761,48 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Выведенная позиция';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Одиночный';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Объединённые';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Воспроизвести';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Пауза';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Повтор';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Предыдущий хоп';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Следующий хоп';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Показать анимацию пакета';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Скрыть анимацию пакета';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Хоп $current из $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Наблюдаемые маршруты: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Основной';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Альт $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4584,8 +4810,10 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count хопов',
|
||||
many: '$count хопов',
|
||||
few: '$count хопа',
|
||||
one: '$count хоп',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4596,14 +4824,14 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Общий сегмент';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Расчётный сегмент';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Используется в $count маршрутах';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4611,33 +4839,235 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count хопов не имеют координат — показанный путь неполный',
|
||||
many: '$count хопов не имеют координат — показанный путь неполный',
|
||||
few: '$count хопа не имеют координат — показанный путь неполный',
|
||||
one: '$count хоп не имеет координат — показанный путь неполный',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Показать всё';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Скрыть путь';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Показать маршрут';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Скрыть панель';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Расширить панель';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Нет координат';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Следить за пакетом';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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 с';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Rýchle prepínač';
|
||||
@@ -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ť';
|
||||
|
||||
@@ -1731,37 +1844,37 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get map_title => 'Mapa uzlov';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Vyhľadajte podľa názvu alebo ID uzla';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Aktivita';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Nedávne';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Neaktuálne';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Viditeľný';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Skrytý';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Nacentrovať na uzol';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Podrobnosti';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Bez GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Nenašli sa žiadne zodpovedajúce uzly.';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Úroveň výhľadu';
|
||||
@@ -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';
|
||||
|
||||
@@ -4501,48 +4723,48 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Odvodená poloha';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Jednotlivý';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Spojené';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Prehrať';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Pozastaviť';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Prehrať znova';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Predchádzajúci skok';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Nasledujúci skok';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Zobraziť animáciu paketu';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Skryť animáciu paketu';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Skok $current z $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Pozorované cesty: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Primárna';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Alternatívny $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4550,8 +4772,9 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count skokov',
|
||||
few: '$count skoky',
|
||||
one: '1 skok',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4562,14 +4785,14 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Spoločný segment';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Odhadovaný segment';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Používané $count cestami';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4577,33 +4800,232 @@ class AppLocalizationsSk extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count skokov nemá polohu — zobrazená trasa je neúplná',
|
||||
few: '$count skoky nemajú polohu — zobrazená trasa je neúplná',
|
||||
one: '1 skok nemá polohu — zobrazená trasa je neúplná',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Zobraziť všetky';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Skryť cestu';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Zobraziť trasu';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Zatvoriť panel';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Rozbaliť panel';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Bez polohy';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Uzamknúť pohľad na paket';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +320,7 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Hitro preklop';
|
||||
@@ -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';
|
||||
|
||||
@@ -1724,37 +1838,37 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get map_title => 'Mapa omrežja';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Iščite ime ali ID vozlišča';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Dejavnost';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
String get map_online => 'V omrežju';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Nedavni';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Zastarelo';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Vidno';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Skrit';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Centriraj na vozlišče';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Podrobnosti';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Brez GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Ni ujemajočih se vozlišč';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Linija vida';
|
||||
@@ -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';
|
||||
|
||||
@@ -4500,48 +4723,48 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Izpeljana lokacija';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Posamično';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Skupno';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Predvajaj';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Premor';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Ponovitev';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Prejšnji skok';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Naslednji skok';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Prikaži animacijo paketa';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Skrij animacijo paketa';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Skok $current od $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Opazovane poti: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Primarna';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Alternativa $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4549,8 +4772,10 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count skokov',
|
||||
few: '$count skoki',
|
||||
two: '2 skoka',
|
||||
one: '1 skok',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4561,14 +4786,14 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Deljen segment';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Ocenjen segment';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Uporablja $count poti';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4576,33 +4801,233 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count skokov nima lokacije — prikazana pot je delna',
|
||||
few: '$count skoki nimajo lokacije — prikazana pot je delna',
|
||||
two: '2 skoka nimata lokacije — prikazana pot je delna',
|
||||
one: '1 skok nima lokacije — prikazana pot je delna',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Pokaži vse';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Skrij pot';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Pokaži pot';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Strni ploščo';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Razširi ploščo';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Brez lokacije';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Zakleni pogled na paket';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,7 +318,7 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Snabb växling';
|
||||
@@ -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';
|
||||
|
||||
@@ -1717,37 +1830,37 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get map_title => 'Nodkarta';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Sök efter nodens namn eller ID';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Aktivitet';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Nyligen';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Inaktuell';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Synlig';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Dold';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Centrera på nod';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Detaljer';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Ingen GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Inga matchande noder';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Synlinje';
|
||||
@@ -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';
|
||||
|
||||
@@ -4474,48 +4696,48 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Antagen position';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Enkel';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Kombinerat';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Spela';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Pausa';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Återspela';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Föregående hopp';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Nästa hopp';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Visa paketanimering';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Dölj paketanimering';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Hopp $current av $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Observerade vägar: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Primär';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Alternativ $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4523,8 +4745,8 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count hopp',
|
||||
one: '1 hopp',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4535,14 +4757,14 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Delat segment';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Uppskattat segment';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Används av $count vägar';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4550,33 +4772,231 @@ class AppLocalizationsSv extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count hopp saknar position — den visade vägen är ofullständig',
|
||||
one: '1 hopp saknar position — den visade vägen är ofullständig',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Visa allt';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Dölj väg';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Visa väg';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Fäll ihop panel';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Expandera panel';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Ingen position';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Lås vy till paket';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,7 +321,7 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
'Bluetooth недоступний у браузері. Підключіться через USB.';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => 'Швидке перемикання';
|
||||
@@ -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 => 'Відповісти';
|
||||
|
||||
@@ -1737,37 +1851,37 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get map_title => 'Карта вузлів';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => 'Назва або ID вузла';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => 'Активність';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
String get map_online => 'Онлайн';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => 'Нещодавні';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => 'Застаріло';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => 'Видимий';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => 'Прихований';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => 'Центрувати на вузлі';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => 'Деталі';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => 'Без GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => 'Не знайдено відповідних вузлів';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => 'Пряма видимість';
|
||||
@@ -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 => 'Увімкнути переклад';
|
||||
|
||||
@@ -4534,48 +4756,48 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => 'Висновок щодо положення';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => 'Один';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => 'Об\'єднаний';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => 'Відтворити';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => 'Призупинити';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => 'Повтор';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => 'Попередній перехід';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => 'Наступний перехід';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => 'Відобразити анімацію пакета';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => 'Приховати анімацію пакета';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return 'Перехід $current з $total';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return 'Зафіксовані маршрути: $count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => 'Основний';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return 'Альт. $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4583,8 +4805,10 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count переходів',
|
||||
many: '$count переходів',
|
||||
few: '$count переходи',
|
||||
one: '1 перехід',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4595,14 +4819,14 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => 'Об\'єднаний сегмент';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => 'Орієнтовний сегмент';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return 'Використовується $count шляхами';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4610,33 +4834,237 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other:
|
||||
'$count переходів не мають геопозиції — показаний шлях є частковим',
|
||||
many: '$count переходів не мають геопозиції — показаний шлях є частковим',
|
||||
few: '$count переходи не мають геопозиції — показаний шлях є частковим',
|
||||
one: '1 перехід не має геопозиції — показаний шлях є частковим',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => 'Показати все';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => 'Приховати шлях';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => 'Показати шлях';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => 'Згорнути панель';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => 'Розгорнути панель';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => 'Без геопозиції';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => 'Прив\'язати вигляд до пакету';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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 с';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -306,8 +306,7 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get scanner_enableBluetooth => '启用蓝牙';
|
||||
|
||||
@override
|
||||
String get scanner_bluetoothWebUnsupported =>
|
||||
'Bluetooth isn\'t available in the browser. Connect over USB instead.';
|
||||
String get scanner_bluetoothWebUnsupported => '浏览器不支持蓝牙,请改用 USB 连接。';
|
||||
|
||||
@override
|
||||
String get device_quickSwitch => '快速切换';
|
||||
@@ -351,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 => '位置';
|
||||
|
||||
@@ -438,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';
|
||||
|
||||
@@ -542,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 => '预设';
|
||||
|
||||
@@ -656,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 => '启用消息追踪';
|
||||
|
||||
@@ -780,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 => '地图显示';
|
||||
|
||||
@@ -833,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 => '离线地图缓存';
|
||||
|
||||
@@ -1246,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 => '暂无消息';
|
||||
|
||||
@@ -1301,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 => '回复';
|
||||
|
||||
@@ -1639,37 +1748,37 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get map_title => '节点地图';
|
||||
|
||||
@override
|
||||
String get map_searchHint => 'Search node name or ID';
|
||||
String get map_searchHint => '搜索节点名称或ID';
|
||||
|
||||
@override
|
||||
String get map_activity => 'Activity';
|
||||
String get map_activity => '活动';
|
||||
|
||||
@override
|
||||
String get map_online => 'Online';
|
||||
String get map_online => '在线';
|
||||
|
||||
@override
|
||||
String get map_recent => 'Recent';
|
||||
String get map_recent => '最近';
|
||||
|
||||
@override
|
||||
String get map_stale => 'Stale';
|
||||
String get map_stale => '过时';
|
||||
|
||||
@override
|
||||
String get map_visible => 'Visible';
|
||||
String get map_visible => '可见';
|
||||
|
||||
@override
|
||||
String get map_hidden => 'Hidden';
|
||||
String get map_hidden => '已隐藏';
|
||||
|
||||
@override
|
||||
String get map_centerOnNode => 'Center on node';
|
||||
String get map_centerOnNode => '以节点为中心';
|
||||
|
||||
@override
|
||||
String get map_details => 'Details';
|
||||
String get map_details => '详细信息';
|
||||
|
||||
@override
|
||||
String get map_noGps => 'No GPS';
|
||||
String get map_noGps => '无 GPS';
|
||||
|
||||
@override
|
||||
String get map_noResults => 'No matching nodes';
|
||||
String get map_noResults => '未找到匹配的节点';
|
||||
|
||||
@override
|
||||
String get map_lineOfSight => '视线';
|
||||
@@ -1926,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,
|
||||
@@ -2440,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 => '洪水(德克萨斯州)延误';
|
||||
@@ -4015,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 => '启用翻译功能';
|
||||
|
||||
@@ -4184,48 +4395,48 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String get pathTrace_legendInferred => '推测的位置';
|
||||
|
||||
@override
|
||||
String get pathMap_viewSingle => 'Single';
|
||||
String get pathMap_viewSingle => '单条';
|
||||
|
||||
@override
|
||||
String get pathMap_viewCombined => 'Combined';
|
||||
String get pathMap_viewCombined => '综合';
|
||||
|
||||
@override
|
||||
String get pathMap_play => 'Play';
|
||||
String get pathMap_play => '播放';
|
||||
|
||||
@override
|
||||
String get pathMap_pause => 'Pause';
|
||||
String get pathMap_pause => '暂停';
|
||||
|
||||
@override
|
||||
String get pathMap_replay => 'Replay';
|
||||
String get pathMap_replay => '重播';
|
||||
|
||||
@override
|
||||
String get pathMap_stepBack => 'Previous hop';
|
||||
String get pathMap_stepBack => '上一跳';
|
||||
|
||||
@override
|
||||
String get pathMap_stepForward => 'Next hop';
|
||||
String get pathMap_stepForward => '下一跳';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOn => 'Show packet animation';
|
||||
String get pathMap_animationOn => '显示数据包动画';
|
||||
|
||||
@override
|
||||
String get pathMap_animationOff => 'Hide packet animation';
|
||||
String get pathMap_animationOff => '隐藏数据包动画';
|
||||
|
||||
@override
|
||||
String pathMap_hopOf(int current, int total) {
|
||||
return 'Hop $current of $total';
|
||||
return '第 $current 跳,共 $total 跳';
|
||||
}
|
||||
|
||||
@override
|
||||
String pathMap_observedPaths(int count) {
|
||||
return 'Observed paths: $count';
|
||||
return '观测到的路径:$count';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_primary => 'Primary';
|
||||
String get pathMap_primary => '主路径';
|
||||
|
||||
@override
|
||||
String pathMap_alternate(int index) {
|
||||
return 'Alt $index';
|
||||
return '备用 $index';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4233,8 +4444,8 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops',
|
||||
one: '1 hop',
|
||||
other: '$count 跳',
|
||||
one: '1 跳',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
@@ -4245,14 +4456,14 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_legendShared => 'Shared segment';
|
||||
String get pathMap_legendShared => '共享路段';
|
||||
|
||||
@override
|
||||
String get pathMap_legendEstimated => 'Estimated segment';
|
||||
String get pathMap_legendEstimated => '估算路段';
|
||||
|
||||
@override
|
||||
String pathMap_sharedNodeCount(int count) {
|
||||
return 'Used by $count paths';
|
||||
return '已被 $count 条路径使用';
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -4260,33 +4471,222 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
String _temp0 = intl.Intl.pluralLogic(
|
||||
count,
|
||||
locale: localeName,
|
||||
other: '$count hops have no location — the shown path is partial',
|
||||
one: '1 hop has no location — the shown path is partial',
|
||||
other: '$count 跳无位置信息 — 显示的路径不完整',
|
||||
one: '1 跳无位置信息 — 显示的路径不完整',
|
||||
);
|
||||
return '$_temp0';
|
||||
}
|
||||
|
||||
@override
|
||||
String get pathMap_showAllPaths => 'Show all';
|
||||
String get pathMap_showAllPaths => '显示全部';
|
||||
|
||||
@override
|
||||
String get pathMap_hidePath => 'Hide path';
|
||||
String get pathMap_hidePath => '隐藏路径';
|
||||
|
||||
@override
|
||||
String get pathMap_showPath => 'Show path';
|
||||
String get pathMap_showPath => '显示路径';
|
||||
|
||||
@override
|
||||
String get pathMap_collapsePanel => 'Collapse panel';
|
||||
String get pathMap_collapsePanel => '收起面板';
|
||||
|
||||
@override
|
||||
String get pathMap_expandPanel => 'Expand panel';
|
||||
String get pathMap_expandPanel => '展开面板';
|
||||
|
||||
@override
|
||||
String get pathMap_noLocation => 'No location';
|
||||
String get pathMap_noLocation => '无位置';
|
||||
|
||||
@override
|
||||
String get pathMap_followPacket => 'Lock view to packet';
|
||||
String get pathMap_followPacket => '锁定视图跟随数据包';
|
||||
|
||||
@override
|
||||
String get pathMap_unfollowPacket => 'Unlock view from packet';
|
||||
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",
|
||||
@@ -2446,5 +2511,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth vereist een Chromium-browser.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS-locatie bevestigd",
|
||||
"pathTrace_legendInferred": "Afgeleide positie"
|
||||
"pathTrace_legendInferred": "Afgeleide positie",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_activity": "Activiteit",
|
||||
"map_searchHint": "Zoek op naam of ID van de knoop",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth is niet beschikbaar in de browser. Verbind dan via USB.",
|
||||
"map_online": "Online",
|
||||
"map_recent": "Recent",
|
||||
"map_stale": "Verouderd",
|
||||
"map_visible": "Zichtbaar",
|
||||
"map_hidden": "Verborgen",
|
||||
"map_centerOnNode": "Centreer op node",
|
||||
"map_details": "Details",
|
||||
"map_noGps": "Geen GPS",
|
||||
"map_noResults": "Geen overeenkomende nodes",
|
||||
"pathMap_viewSingle": "Enkel",
|
||||
"pathMap_viewCombined": "Gezamenlijk",
|
||||
"pathMap_play": "Afspelen",
|
||||
"pathMap_pause": "Pauze",
|
||||
"pathMap_replay": "Herhalen",
|
||||
"pathMap_stepBack": "Vorige hop",
|
||||
"pathMap_stepForward": "Volgende hop",
|
||||
"pathMap_animationOn": "Pakketanimatie tonen",
|
||||
"pathMap_animationOff": "Pakketanimatie verbergen",
|
||||
"pathMap_hopOf": "Hop {current} van {total}",
|
||||
"pathMap_observedPaths": "Waargenomen paden: {count}",
|
||||
"pathMap_primary": "Primair",
|
||||
"pathMap_alternate": "Alternatief {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 hop} other{{count} hops}}",
|
||||
"pathMap_legendShared": "Gedeeld segment",
|
||||
"pathMap_legendEstimated": "Geschat segment",
|
||||
"pathMap_sharedNodeCount": "Gebruikt door {count} paden",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 hop heeft geen locatie — het weergegeven pad is onvolledig} other{{count} hops hebben geen locatie — het weergegeven pad is onvolledig}}",
|
||||
"pathMap_showAllPaths": "Toon alles",
|
||||
"pathMap_hidePath": "Verberg pad",
|
||||
"pathMap_showPath": "Toon pad",
|
||||
"pathMap_collapsePanel": "Paneel inklappen",
|
||||
"pathMap_expandPanel": "Paneel uitklappen",
|
||||
"pathMap_noLocation": "Geen locatie",
|
||||
"pathMap_followPacket": "Weergave vergrendelen op pakket",
|
||||
"pathMap_unfollowPacket": "Weergave ontgrendelen van pakket",
|
||||
"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",
|
||||
@@ -2484,5 +2549,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth wymaga przeglądarki Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS potwierdzone",
|
||||
"pathTrace_legendInferred": "Wywnioskowana pozycja"
|
||||
"pathTrace_legendInferred": "Wywnioskowana pozycja",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"map_searchHint": "Wyszukaj nazwę lub identyfikator węzła",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth nie jest dostępny w przeglądarce. Połącz się przez USB.",
|
||||
"map_activity": "Aktywność",
|
||||
"map_recent": "Ostatnie",
|
||||
"map_stale": "Nieaktualne",
|
||||
"map_visible": "Widoczny",
|
||||
"map_hidden": "Ukryty",
|
||||
"map_centerOnNode": "Wyśrodkuj na węźle",
|
||||
"map_details": "Szczegóły",
|
||||
"map_noGps": "Brak GPS",
|
||||
"map_noResults": "Brak pasujących węzłów",
|
||||
"pathMap_viewSingle": "Pojedyncza",
|
||||
"pathMap_viewCombined": "Połączone",
|
||||
"pathMap_play": "Odtwórz",
|
||||
"pathMap_pause": "Wstrzymaj",
|
||||
"pathMap_replay": "Odtwórz ponownie",
|
||||
"pathMap_stepBack": "Poprzedni skok",
|
||||
"pathMap_stepForward": "Następny skok",
|
||||
"pathMap_animationOn": "Pokaż animację pakietu",
|
||||
"pathMap_animationOff": "Ukryj animację pakietu",
|
||||
"pathMap_hopOf": "Skok {current} z {total}",
|
||||
"pathMap_observedPaths": "Obserwowane trasy: {count}",
|
||||
"pathMap_primary": "Główna",
|
||||
"pathMap_alternate": "Alt. {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoki} many{{count} skoków} other{{count} skoku}}",
|
||||
"pathMap_legendShared": "Wspólny segment",
|
||||
"pathMap_legendEstimated": "Szacunkowy segment",
|
||||
"pathMap_sharedNodeCount": "Wykorzystywane przez {count} ścieżek",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 skok nie ma lokalizacji — pokazana ścieżka jest niekompletna} few{{count} skoki nie mają lokalizacji — pokazana ścieżka jest niekompletna} many{{count} skoków nie ma lokalizacji — pokazana ścieżka jest niekompletna} other{{count} skoku nie ma lokalizacji — pokazana ścieżka jest niekompletna}}",
|
||||
"pathMap_showAllPaths": "Pokaż wszystkie",
|
||||
"pathMap_hidePath": "Ukryj ścieżkę",
|
||||
"pathMap_showPath": "Wyświetl trasę",
|
||||
"pathMap_collapsePanel": "Zwiń panel",
|
||||
"pathMap_expandPanel": "Rozwiń panel",
|
||||
"pathMap_noLocation": "Brak lokalizacji",
|
||||
"pathMap_followPacket": "Śledź pakiet",
|
||||
"pathMap_unfollowPacket": "Przestań śledzić pakiet",
|
||||
"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",
|
||||
@@ -2446,5 +2511,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "O Web Bluetooth requer um navegador Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS confirmado",
|
||||
"pathTrace_legendInferred": "Posição inferida"
|
||||
"pathTrace_legendInferred": "Posição inferida",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"map_activity": "Atividade",
|
||||
"scanner_bluetoothWebUnsupported": "A funcionalidade Bluetooth não está disponível no navegador. Conecte-se via USB em vez disso.",
|
||||
"map_searchHint": "Pesquisar por nome ou ID do nó",
|
||||
"map_recent": "Recente",
|
||||
"map_stale": "Vencido",
|
||||
"map_visible": "Visível",
|
||||
"map_hidden": "Escondido",
|
||||
"map_centerOnNode": "Centralizar no nó",
|
||||
"map_details": "Detalhes",
|
||||
"map_noGps": "Sem GPS",
|
||||
"map_noResults": "Nenhum nó encontrado",
|
||||
"pathMap_viewSingle": "Único",
|
||||
"pathMap_viewCombined": "Combinado",
|
||||
"pathMap_play": "Reproduzir",
|
||||
"pathMap_pause": "Pausa",
|
||||
"pathMap_stepBack": "Salto anterior",
|
||||
"pathMap_replay": "Repetir",
|
||||
"pathMap_stepForward": "Próximo salto",
|
||||
"pathMap_animationOn": "Exibir animação do pacote",
|
||||
"pathMap_animationOff": "Ocultar a animação do pacote",
|
||||
"pathMap_hopOf": "Salto {current} de {total}",
|
||||
"pathMap_observedPaths": "Caminhos observados: {count}",
|
||||
"pathMap_primary": "Primário",
|
||||
"pathMap_alternate": "Alt {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
|
||||
"pathMap_legendShared": "Segmento compartilhado",
|
||||
"pathMap_legendEstimated": "Segmento estimado",
|
||||
"pathMap_sharedNodeCount": "Utilizado em {count} caminhos",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 salto não tem localização — o caminho mostrado é parcial} other{{count} saltos não têm localização — o caminho mostrado é parcial}}",
|
||||
"pathMap_showAllPaths": "Mostrar tudo",
|
||||
"pathMap_hidePath": "Esconder caminho",
|
||||
"pathMap_showPath": "Mostrar o caminho",
|
||||
"pathMap_collapsePanel": "Recolher painel",
|
||||
"pathMap_expandPanel": "Expandir painel",
|
||||
"pathMap_noLocation": "Sem localização",
|
||||
"pathMap_followPacket": "Fixar vista no pacote",
|
||||
"pathMap_unfollowPacket": "Liberar vista do pacote",
|
||||
"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",
|
||||
@@ -1749,5 +1814,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Для работы Web Bluetooth требуется браузер на основе Chromium.",
|
||||
"channels_communityShortId": "Идентификатор: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS подтверждено",
|
||||
"pathTrace_legendInferred": "Выведенная позиция"
|
||||
"pathTrace_legendInferred": "Выведенная позиция",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_searchHint": "Поиск по имени или ID узла",
|
||||
"map_online": "Онлайн",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth недоступен в браузере. Подключитесь через USB.",
|
||||
"map_activity": "Активность",
|
||||
"map_recent": "Недавно",
|
||||
"map_stale": "Устаревший",
|
||||
"map_visible": "Видимый",
|
||||
"map_hidden": "Скрытый",
|
||||
"map_centerOnNode": "Центрировать на узле",
|
||||
"map_details": "Детали",
|
||||
"map_noGps": "Без GPS",
|
||||
"map_noResults": "Не найдено соответствующих узлов",
|
||||
"pathMap_viewSingle": "Одиночный",
|
||||
"pathMap_viewCombined": "Объединённые",
|
||||
"pathMap_play": "Воспроизвести",
|
||||
"pathMap_pause": "Пауза",
|
||||
"pathMap_replay": "Повтор",
|
||||
"pathMap_stepBack": "Предыдущий хоп",
|
||||
"pathMap_stepForward": "Следующий хоп",
|
||||
"pathMap_animationOn": "Показать анимацию пакета",
|
||||
"pathMap_animationOff": "Скрыть анимацию пакета",
|
||||
"pathMap_hopOf": "Хоп {current} из {total}",
|
||||
"pathMap_observedPaths": "Наблюдаемые маршруты: {count}",
|
||||
"pathMap_primary": "Основной",
|
||||
"pathMap_alternate": "Альт {index}",
|
||||
"pathMap_hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопов}}",
|
||||
"pathMap_legendShared": "Общий сегмент",
|
||||
"pathMap_legendEstimated": "Расчётный сегмент",
|
||||
"pathMap_sharedNodeCount": "Используется в {count} маршрутах",
|
||||
"pathMap_partialAnimation": "{count, plural, one{{count} хоп не имеет координат — показанный путь неполный} few{{count} хопа не имеют координат — показанный путь неполный} many{{count} хопов не имеют координат — показанный путь неполный} other{{count} хопов не имеют координат — показанный путь неполный}}",
|
||||
"pathMap_showAllPaths": "Показать всё",
|
||||
"pathMap_hidePath": "Скрыть путь",
|
||||
"pathMap_collapsePanel": "Скрыть панель",
|
||||
"pathMap_showPath": "Показать маршрут",
|
||||
"pathMap_expandPanel": "Расширить панель",
|
||||
"pathMap_noLocation": "Нет координат",
|
||||
"pathMap_followPacket": "Следить за пакетом",
|
||||
"pathMap_unfollowPacket": "Не следить за пакетом",
|
||||
"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",
|
||||
@@ -2446,5 +2511,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth vyžaduje prehliadač Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS potvrdilo",
|
||||
"pathTrace_legendInferred": "Odvodená poloha"
|
||||
"pathTrace_legendInferred": "Odvodená poloha",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"scanner_bluetoothWebUnsupported": "Funkcia Bluetooth nie je dostupná v prehliadači. Prepojte sa pomocou USB.",
|
||||
"map_searchHint": "Vyhľadajte podľa názvu alebo ID uzla",
|
||||
"map_activity": "Aktivita",
|
||||
"map_recent": "Nedávne",
|
||||
"map_stale": "Neaktuálne",
|
||||
"map_hidden": "Skrytý",
|
||||
"map_visible": "Viditeľný",
|
||||
"map_centerOnNode": "Nacentrovať na uzol",
|
||||
"map_details": "Podrobnosti",
|
||||
"map_noGps": "Bez GPS",
|
||||
"map_noResults": "Nenašli sa žiadne zodpovedajúce uzly.",
|
||||
"pathMap_viewSingle": "Jednotlivý",
|
||||
"pathMap_viewCombined": "Spojené",
|
||||
"pathMap_play": "Prehrať",
|
||||
"pathMap_pause": "Pozastaviť",
|
||||
"pathMap_replay": "Prehrať znova",
|
||||
"pathMap_stepBack": "Predchádzajúci skok",
|
||||
"pathMap_stepForward": "Nasledujúci skok",
|
||||
"pathMap_animationOn": "Zobraziť animáciu paketu",
|
||||
"pathMap_animationOff": "Skryť animáciu paketu",
|
||||
"pathMap_hopOf": "Skok {current} z {total}",
|
||||
"pathMap_observedPaths": "Pozorované cesty: {count}",
|
||||
"pathMap_primary": "Primárna",
|
||||
"pathMap_alternate": "Alternatívny {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 skok} few{{count} skoky} other{{count} skokov}}",
|
||||
"pathMap_legendShared": "Spoločný segment",
|
||||
"pathMap_legendEstimated": "Odhadovaný segment",
|
||||
"pathMap_sharedNodeCount": "Používané {count} cestami",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 skok nemá polohu — zobrazená trasa je neúplná} few{{count} skoky nemajú polohu — zobrazená trasa je neúplná} other{{count} skokov nemá polohu — zobrazená trasa je neúplná}}",
|
||||
"pathMap_showAllPaths": "Zobraziť všetky",
|
||||
"pathMap_hidePath": "Skryť cestu",
|
||||
"pathMap_showPath": "Zobraziť trasu",
|
||||
"pathMap_collapsePanel": "Zatvoriť panel",
|
||||
"pathMap_expandPanel": "Rozbaliť panel",
|
||||
"pathMap_noLocation": "Bez polohy",
|
||||
"pathMap_followPacket": "Uzamknúť pohľad na paket",
|
||||
"pathMap_unfollowPacket": "Odomknúť pohľad od paketu",
|
||||
"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",
|
||||
@@ -2446,5 +2511,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth zahteva brskalnik Chromium.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS potrdilo",
|
||||
"pathTrace_legendInferred": "Izpeljana lokacija"
|
||||
"pathTrace_legendInferred": "Izpeljana lokacija",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_bluetoothWebUnsupported": "Funkcija Bluetooth v brskalniku ni na voljo. Povežite se preko USB-ja namesto tega.",
|
||||
"map_searchHint": "Iščite ime ali ID vozlišča",
|
||||
"map_online": "V omrežju",
|
||||
"map_activity": "Dejavnost",
|
||||
"map_recent": "Nedavni",
|
||||
"map_stale": "Zastarelo",
|
||||
"map_visible": "Vidno",
|
||||
"map_hidden": "Skrit",
|
||||
"map_centerOnNode": "Centriraj na vozlišče",
|
||||
"map_details": "Podrobnosti",
|
||||
"map_noGps": "Brez GPS",
|
||||
"map_noResults": "Ni ujemajočih se vozlišč",
|
||||
"pathMap_viewSingle": "Posamično",
|
||||
"pathMap_viewCombined": "Skupno",
|
||||
"pathMap_play": "Predvajaj",
|
||||
"pathMap_pause": "Premor",
|
||||
"pathMap_replay": "Ponovitev",
|
||||
"pathMap_stepBack": "Prejšnji skok",
|
||||
"pathMap_stepForward": "Naslednji skok",
|
||||
"pathMap_animationOn": "Prikaži animacijo paketa",
|
||||
"pathMap_animationOff": "Skrij animacijo paketa",
|
||||
"pathMap_hopOf": "Skok {current} od {total}",
|
||||
"pathMap_observedPaths": "Opazovane poti: {count}",
|
||||
"pathMap_primary": "Primarna",
|
||||
"pathMap_alternate": "Alternativa {index}",
|
||||
"pathMap_legendShared": "Deljen segment",
|
||||
"pathMap_legendEstimated": "Ocenjen segment",
|
||||
"pathMap_sharedNodeCount": "Uporablja {count} poti",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 skok nima lokacije — prikazana pot je delna} =2{2 skoka nimata lokacije — prikazana pot je delna} few{{count} skoki nimajo lokacije — prikazana pot je delna} other{{count} skokov nima lokacije — prikazana pot je delna}}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 skok} =2{2 skoka} few{{count} skoki} other{{count} skokov}}",
|
||||
"pathMap_showAllPaths": "Pokaži vse",
|
||||
"pathMap_hidePath": "Skrij pot",
|
||||
"pathMap_showPath": "Pokaži pot",
|
||||
"pathMap_collapsePanel": "Strni ploščo",
|
||||
"pathMap_expandPanel": "Razširi ploščo",
|
||||
"pathMap_noLocation": "Brez lokacije",
|
||||
"pathMap_followPacket": "Zakleni pogled na paket",
|
||||
"pathMap_unfollowPacket": "Odkleni pogled od paketa",
|
||||
"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",
|
||||
@@ -2446,5 +2511,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth kräver en Chromium-baserad webbläsare.",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS-verifierat",
|
||||
"pathTrace_legendInferred": "Antagen position"
|
||||
"pathTrace_legendInferred": "Antagen position",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "Online",
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth är inte tillgängligt i webbläsaren. Anslut istället via USB.",
|
||||
"map_activity": "Aktivitet",
|
||||
"map_searchHint": "Sök efter nodens namn eller ID",
|
||||
"map_recent": "Nyligen",
|
||||
"map_stale": "Inaktuell",
|
||||
"map_visible": "Synlig",
|
||||
"map_hidden": "Dold",
|
||||
"map_centerOnNode": "Centrera på nod",
|
||||
"map_details": "Detaljer",
|
||||
"map_noGps": "Ingen GPS",
|
||||
"map_noResults": "Inga matchande noder",
|
||||
"pathMap_viewSingle": "Enkel",
|
||||
"pathMap_viewCombined": "Kombinerat",
|
||||
"pathMap_play": "Spela",
|
||||
"pathMap_pause": "Pausa",
|
||||
"pathMap_replay": "Återspela",
|
||||
"pathMap_stepBack": "Föregående hopp",
|
||||
"pathMap_stepForward": "Nästa hopp",
|
||||
"pathMap_animationOn": "Visa paketanimering",
|
||||
"pathMap_animationOff": "Dölj paketanimering",
|
||||
"pathMap_hopOf": "Hopp {current} av {total}",
|
||||
"pathMap_observedPaths": "Observerade vägar: {count}",
|
||||
"pathMap_primary": "Primär",
|
||||
"pathMap_alternate": "Alternativ {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 hopp} other{{count} hopp}}",
|
||||
"pathMap_legendShared": "Delat segment",
|
||||
"pathMap_legendEstimated": "Uppskattat segment",
|
||||
"pathMap_sharedNodeCount": "Används av {count} vägar",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 hopp saknar position — den visade vägen är ofullständig} other{{count} hopp saknar position — den visade vägen är ofullständig}}",
|
||||
"pathMap_showAllPaths": "Visa allt",
|
||||
"pathMap_hidePath": "Dölj väg",
|
||||
"pathMap_showPath": "Visa väg",
|
||||
"pathMap_collapsePanel": "Fäll ihop panel",
|
||||
"pathMap_expandPanel": "Expandera panel",
|
||||
"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",
|
||||
"@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",
|
||||
@@ -2426,5 +2491,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth вимагає браузера на основі Chromium",
|
||||
"channels_communityShortId": "ID: {id}...",
|
||||
"pathTrace_legendGpsConfirmed": "GPS підтверджено",
|
||||
"pathTrace_legendInferred": "Висновок щодо положення"
|
||||
"pathTrace_legendInferred": "Висновок щодо положення",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"scanner_bluetoothWebUnsupported": "Bluetooth недоступний у браузері. Підключіться через USB.",
|
||||
"map_searchHint": "Назва або ID вузла",
|
||||
"map_activity": "Активність",
|
||||
"map_online": "Онлайн",
|
||||
"map_recent": "Нещодавні",
|
||||
"map_stale": "Застаріло",
|
||||
"map_visible": "Видимий",
|
||||
"map_hidden": "Прихований",
|
||||
"map_centerOnNode": "Центрувати на вузлі",
|
||||
"map_details": "Деталі",
|
||||
"map_noGps": "Без GPS",
|
||||
"map_noResults": "Не знайдено відповідних вузлів",
|
||||
"pathMap_viewSingle": "Один",
|
||||
"pathMap_viewCombined": "Об'єднаний",
|
||||
"pathMap_play": "Відтворити",
|
||||
"pathMap_pause": "Призупинити",
|
||||
"pathMap_replay": "Повтор",
|
||||
"pathMap_stepForward": "Наступний перехід",
|
||||
"pathMap_stepBack": "Попередній перехід",
|
||||
"pathMap_animationOn": "Відобразити анімацію пакета",
|
||||
"pathMap_animationOff": "Приховати анімацію пакета",
|
||||
"pathMap_hopOf": "Перехід {current} з {total}",
|
||||
"pathMap_observedPaths": "Зафіксовані маршрути: {count}",
|
||||
"pathMap_primary": "Основний",
|
||||
"pathMap_alternate": "Альт. {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 перехід} few{{count} переходи} many{{count} переходів} other{{count} переходів}}",
|
||||
"pathMap_legendShared": "Об'єднаний сегмент",
|
||||
"pathMap_legendEstimated": "Орієнтовний сегмент",
|
||||
"pathMap_sharedNodeCount": "Використовується {count} шляхами",
|
||||
"pathMap_partialAnimation": "{count, plural, =1{1 перехід не має геопозиції — показаний шлях є частковим} few{{count} переходи не мають геопозиції — показаний шлях є частковим} many{{count} переходів не мають геопозиції — показаний шлях є частковим} other{{count} переходів не мають геопозиції — показаний шлях є частковим}}",
|
||||
"pathMap_showAllPaths": "Показати все",
|
||||
"pathMap_hidePath": "Приховати шлях",
|
||||
"pathMap_showPath": "Показати шлях",
|
||||
"pathMap_collapsePanel": "Згорнути панель",
|
||||
"pathMap_expandPanel": "Розгорнути панель",
|
||||
"pathMap_noLocation": "Без геопозиції",
|
||||
"pathMap_followPacket": "Прив'язати вигляд до пакету",
|
||||
"pathMap_unfollowPacket": "Відв'язати вигляд від пакету",
|
||||
"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 延迟",
|
||||
@@ -2451,5 +2516,321 @@
|
||||
"chrome_bluetoothRequiresChromium": "Web Bluetooth 需要 Chromium 浏览器",
|
||||
"channels_communityShortId": "ID:{id}...",
|
||||
"pathTrace_legendGpsConfirmed": "通过GPS确认",
|
||||
"pathTrace_legendInferred": "推测的位置"
|
||||
"pathTrace_legendInferred": "推测的位置",
|
||||
"@pathMap_hopOf": {
|
||||
"placeholders": {
|
||||
"current": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_observedPaths": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_alternate": {
|
||||
"placeholders": {
|
||||
"index": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_hopCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_gpsCount": {
|
||||
"placeholders": {
|
||||
"confirmed": {
|
||||
"type": "int"
|
||||
},
|
||||
"total": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_sharedNodeCount": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"@pathMap_partialAnimation": {
|
||||
"placeholders": {
|
||||
"count": {
|
||||
"type": "int"
|
||||
}
|
||||
}
|
||||
},
|
||||
"map_online": "在线",
|
||||
"scanner_bluetoothWebUnsupported": "浏览器不支持蓝牙,请改用 USB 连接。",
|
||||
"map_activity": "活动",
|
||||
"map_searchHint": "搜索节点名称或ID",
|
||||
"map_recent": "最近",
|
||||
"map_visible": "可见",
|
||||
"map_stale": "过时",
|
||||
"map_hidden": "已隐藏",
|
||||
"map_centerOnNode": "以节点为中心",
|
||||
"map_details": "详细信息",
|
||||
"map_noGps": "无 GPS",
|
||||
"map_noResults": "未找到匹配的节点",
|
||||
"pathMap_viewSingle": "单条",
|
||||
"pathMap_viewCombined": "综合",
|
||||
"pathMap_play": "播放",
|
||||
"pathMap_pause": "暂停",
|
||||
"pathMap_replay": "重播",
|
||||
"pathMap_stepBack": "上一跳",
|
||||
"pathMap_stepForward": "下一跳",
|
||||
"pathMap_animationOn": "显示数据包动画",
|
||||
"pathMap_animationOff": "隐藏数据包动画",
|
||||
"pathMap_hopOf": "第 {current} 跳,共 {total} 跳",
|
||||
"pathMap_observedPaths": "观测到的路径:{count}",
|
||||
"pathMap_primary": "主路径",
|
||||
"pathMap_alternate": "备用 {index}",
|
||||
"pathMap_hopCount": "{count, plural, =1{1 跳} other{{count} 跳}}",
|
||||
"pathMap_legendShared": "共享路段",
|
||||
"pathMap_legendEstimated": "估算路段",
|
||||
"pathMap_sharedNodeCount": "已被 {count} 条路径使用",
|
||||
"pathMap_showAllPaths": "显示全部",
|
||||
"pathMap_hidePath": "隐藏路径",
|
||||
"pathMap_showPath": "显示路径",
|
||||
"pathMap_collapsePanel": "收起面板",
|
||||
"pathMap_expandPanel": "展开面板",
|
||||
"pathMap_noLocation": "无位置",
|
||||
"pathMap_followPacket": "锁定视图跟随数据包",
|
||||
"pathMap_unfollowPacket": "解锁视图跟随",
|
||||
"pathMap_gpsCount": "{confirmed}/{total} GPS",
|
||||
"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}) {
|
||||
@@ -1466,114 +1515,136 @@ class _ChannelMessagePathMapScreenState
|
||||
border: Border.all(color: MeshPalette.line2),
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 4, 0),
|
||||
child: Row(
|
||||
child: DefaultTextStyle(
|
||||
style: const TextStyle(color: MeshPalette.ink),
|
||||
child: IconTheme(
|
||||
data: const IconThemeData(color: MeshPalette.ink),
|
||||
child: TextButtonTheme(
|
||||
data: TextButtonThemeData(
|
||||
style: TextButton.styleFrom(foregroundColor: MeshPalette.ink),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 8, 4, 0),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.channelPath_repeaterHops,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
l10n.channelPath_repeaterHops,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatDistance(
|
||||
selectedDisplay?.distanceMeters ??
|
||||
_pathDistance,
|
||||
isImperial: isImperial,
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: MeshPalette.ink2,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
formatDistance(
|
||||
selectedDisplay?.distanceMeters ??
|
||||
_pathDistance,
|
||||
isImperial: isImperial,
|
||||
const SizedBox(height: 4),
|
||||
PathMiniLegend(
|
||||
combined: combined,
|
||||
showInferred: false,
|
||||
),
|
||||
style: MeshTheme.mono(
|
||||
fontSize: 12,
|
||||
color: MeshPalette.ink2,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
PathMiniLegend(
|
||||
combined: combined,
|
||||
showInferred: false,
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(
|
||||
_panelCollapsed
|
||||
? Icons.expand_less
|
||||
: Icons.expand_more,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: _panelCollapsed
|
||||
? l10n.pathMap_expandPanel
|
||||
: l10n.pathMap_collapsePanel,
|
||||
onPressed: () => setState(
|
||||
() => _panelCollapsed = !_panelCollapsed,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: Icon(
|
||||
_panelCollapsed ? Icons.expand_less : Icons.expand_more,
|
||||
size: 20,
|
||||
),
|
||||
tooltip: _panelCollapsed
|
||||
? l10n.pathMap_expandPanel
|
||||
: l10n.pathMap_collapsePanel,
|
||||
onPressed: () =>
|
||||
setState(() => _panelCollapsed = !_panelCollapsed),
|
||||
PathAnimationControls(
|
||||
playback: _playback,
|
||||
selected: selectedDisplay,
|
||||
animationEnabled: _animationEnabled,
|
||||
onToggleAnimation: () => setState(() {
|
||||
_animationEnabled = !_animationEnabled;
|
||||
if (!_animationEnabled) _playback.stop();
|
||||
}),
|
||||
followEnabled: _followPacket,
|
||||
onToggleFollow: _toggleFollowPacket,
|
||||
),
|
||||
if (!_panelCollapsed) ...[
|
||||
if (selectedDisplay != null &&
|
||||
selectedDisplay.unresolvedHops > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 4),
|
||||
child: Text(
|
||||
l10n.pathMap_partialAnimation(
|
||||
selectedDisplay.unresolvedHops,
|
||||
),
|
||||
style: TextStyle(
|
||||
fontSize: 10.5,
|
||||
color: MeshPalette.warn,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (combined)
|
||||
PathSummaryList(
|
||||
paths: entries.map((e) => e.display).toList(),
|
||||
selectedId: selectedDisplay?.id ?? '',
|
||||
hiddenIds: _hiddenPathIds,
|
||||
isImperial: isImperial,
|
||||
onSelect: (display) {
|
||||
for (final entry in entries) {
|
||||
if (entry.display.id == display.id) {
|
||||
_selectEntry(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onToggleVisibility: (display) =>
|
||||
_togglePathVisibility(
|
||||
display,
|
||||
entries,
|
||||
selectedDisplay,
|
||||
),
|
||||
onShowAll: () => setState(_hiddenPathIds.clear),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: _buildHopListView(
|
||||
hops,
|
||||
selectedDisplay,
|
||||
hopUseCount,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
PathAnimationControls(
|
||||
playback: _playback,
|
||||
selected: selectedDisplay,
|
||||
animationEnabled: _animationEnabled,
|
||||
onToggleAnimation: () => setState(() {
|
||||
_animationEnabled = !_animationEnabled;
|
||||
if (!_animationEnabled) _playback.stop();
|
||||
}),
|
||||
followEnabled: _followPacket,
|
||||
onToggleFollow: _toggleFollowPacket,
|
||||
),
|
||||
if (!_panelCollapsed) ...[
|
||||
if (selectedDisplay != null &&
|
||||
selectedDisplay.unresolvedHops > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(12, 0, 12, 4),
|
||||
child: Text(
|
||||
l10n.pathMap_partialAnimation(
|
||||
selectedDisplay.unresolvedHops,
|
||||
),
|
||||
style: TextStyle(fontSize: 10.5, color: MeshPalette.warn),
|
||||
),
|
||||
),
|
||||
if (combined)
|
||||
PathSummaryList(
|
||||
paths: entries.map((e) => e.display).toList(),
|
||||
selectedId: selectedDisplay?.id ?? '',
|
||||
hiddenIds: _hiddenPathIds,
|
||||
isImperial: isImperial,
|
||||
onSelect: (display) {
|
||||
for (final entry in entries) {
|
||||
if (entry.display.id == display.id) {
|
||||
_selectEntry(entry);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
onToggleVisibility: (display) => _togglePathVisibility(
|
||||
display,
|
||||
entries,
|
||||
selectedDisplay,
|
||||
),
|
||||
onShowAll: () => setState(_hiddenPathIds.clear),
|
||||
),
|
||||
const Divider(height: 1),
|
||||
Expanded(
|
||||
child: _buildHopListView(hops, selectedDisplay, hopUseCount),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -1716,6 +1787,7 @@ class _PathHop {
|
||||
final Contact? contact;
|
||||
final LatLng? position;
|
||||
final AppLocalizations l10n;
|
||||
final Uint8List? hopBytes;
|
||||
|
||||
const _PathHop({
|
||||
required this.index,
|
||||
@@ -1723,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)}';
|
||||
}
|
||||
}
|
||||
@@ -1743,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!)
|
||||
@@ -1756,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,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1788,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,
|
||||
|
||||