Compare commits

..

123 Commits

Author SHA1 Message Date
zjs81 c78c77309e Merge remote-tracking branch 'origin/main' 2026-08-11 00:34:19 -07:00
zjs81 724696cd3c Remove 'jni' from FLUTTER_FFI_PLUGIN_LIST in generated_plugins.cmake files for Linux and Windows
Update version in pubspec.yaml to 9.5.0+16
2026-08-11 00:17:02 -07:00
zjs81 cf6c93c50a rebuild translations 2026-08-11 00:15:45 -07:00
Zach eee72b4c65 Add ability to send images over lora 2026-08-10 23:20:46 -07:00
Zach 736eb064bf Update RadioSettings preset with new spreading factor and coding rate 2026-08-09 19:55:01 -07:00
zjs81 9e6968aa04 Merge pull request #509 from ericszimmermann/ez_maps_high_contrast_PR
add High Contrast filter for maps
2026-08-09 19:35:15 -07:00
zjs81 8904fc7208 Merge pull request #514 from Hacuchino-hash/device-hardware-info
Show connected node hardware and firmware in device info
2026-08-09 19:34:31 -07:00
zjs81 02d3325d0d Merge pull request #517 from Hacuchino-hash/hv-battery-chemistry
Add high voltage LiPo battery chemistry option
2026-08-09 19:31:12 -07:00
Hacuchino-hash 3a08f52fd4 Add high voltage LiPo battery chemistry option 2026-08-07 08:02:01 -05:00
Hacuchino-hash 3a9c14e946 Show connected node hardware and firmware in device info 2026-08-07 08:02:00 -05:00
ericz b15f6450bd dart format 2026-08-04 17:27:51 +02:00
ericz 2879bf6147 fix test/screen/path_trace_test.dart 2026-08-04 17:23:50 +02:00
ericz 36f12ebded add High Contrast filter for maps 2026-07-30 19:26:33 +02:00
zjs81 98f6cf3745 Merge pull request #506 from Warmax356/patch-1
Add SWHID and Archive badges to README
2026-07-28 07:51:25 -07:00
WarMax356 35e4d7fa6c Add SWHID and Archive badges to README
Added SWHID and Archive badges to the README.
2026-07-23 13:04:58 +02:00
zjs81 0fe2502309 Merge pull request #450 from PacoX/dev
Add Support for Multi-Byte Routing Paths
2026-07-09 07:14:53 -07:00
PacoX a8cac76491 resolved conflict and update doc 2026-07-09 09:08:50 +02:00
PacoX 2df17be673 remplace onReorder by onReorderItem 2026-07-09 08:43:55 +02:00
Zach 71fa8c17a4 Add cap for discovered contacts to manage memory usage 2026-07-08 20:48:09 -07:00
Zach 698a69dc03 Improve path formatting in message bubble to use zero-width space for better visual presentation 2026-07-08 20:43:58 -07:00
Zach 8765d0f4cc Add localization for 'Received a GIF' in multiple languages and update chat UI 2026-07-08 20:38:06 -07:00
zjs81 c683dbd8f1 Update funding sources in FUNDING.yml 2026-07-08 20:07:45 -07:00
zjs81 b8e113d269 Merge pull request #467 from sethoscope/set-repeater-key
Add UI for setting repeater identity keys
2026-07-06 09:23:01 -07:00
Seth Golub e963248e37 Stop repeater identity key search when user navigates away
Terminate a search for a repeater key with a specified prefix if the
user navigates away from the settings screen.

This commit also adds dispose() calls for the new repeater key input
controllers, which were previously omitted.
2026-07-04 18:54:27 -07:00
Seth Golub 68d2250afc Fix randomBytes to use specified length
Previously we were creating a 32-byte list no matter what was asked for.
2026-07-04 18:34:11 -07:00
Zach f4cca2fb1d Add ExportOptions.plist for App Store Connect configuration
- Created a new ExportOptions.plist file to define export settings for app distribution.
- Configured the plist with method, team ID, signing style, symbol upload option, and destination.
2026-07-04 00:45:18 -07:00
zjs81 0f3961f3a2 Merge pull request #461 from ericszimmermann/ez_alternative_maps
Alternative Maps
2026-07-04 00:32:58 -07:00
zjs81 08130350d7 Merge pull request #497 from ericszimmermann/ez_zerohop_location_advert_PR
Zero-Hop Location advert
2026-07-04 00:31:52 -07:00
ericszimmermann c1dea86dd1 Merge branch 'zjs81:dev' into ez_zerohop_location_advert_PR 2026-07-01 07:27:44 +02:00
zjs81 e1b8d6ec97 Merge pull request #491 from km-foss/chore/au-mid-preset
Add Australia (Mid) Radio Preset
2026-06-25 18:03:34 -07:00
PacoX 3541b1bbac Fix room login screen 2026-06-24 14:38:35 +02:00
PacoX ef6c1e0898 Fix login screen 2026-06-24 14:24:12 +02:00
km-foss eb89ad1d50 chore: Add Australia (Mid) Radio Preset
This mirrors the config option from the official Meshcore app, and represents the preset now commonly in use in New South Wales.
2026-06-24 09:21:45 +10:00
ericz 5ef976b3b5 Add option for automatic zerohop adverts on posision change. 2026-06-23 18:19:22 +02:00
zjs81 7b4f90d646 Merge pull request #484 from hamid-elaosta/icon-fix
Fix adaptive icon and app label on Android
2026-06-19 22:30:34 -07:00
Seth Golub e1a4900dc0 Add UI for setting repeater identity keys
This adds a simple UI in repeater management settings for remotely
setting a repeater's private key. A key can be pasted from an external
key generator, or a random one can be generated here, optionally with a
specified prefix.

If a prefix is specified, searching is done in 30ms batches in order
to keep the UI responsive and show the progress indicator animation
smoothly. Searches can be interrupted. An estimation of search length is
given as expected number of generated key pairs. (A future improvement
would be to observe keys per second performance and give time estimates
based on that.)

When it generates a new private key on your behalf, it can tell you
what the corresponding public key will be. Unfortunately, if you paste
one in from another source, it can't compute the corresponding public
key. This is because the standard Dart crypto library doesn't provide
that capability, nor does it give access to the elliptic curve functions
it implements so we could do it ourselves. Our only recourse would be to
copy the source for all those things, and I didn't think it was worth
it.

This will not check the validity of a pasted key, but the remote
repeater will. It does limit it to the correct length and hexadecimal
charset.

Prefixes are limited to 4 characters. There's no technical reason for
this, but users are unlikely to want more, and I think it would just be
a bad experience to allow more and spin for a very long time. If someone
really wants a longer one, they can use an external generator like they
do now.

Possible TODO: rewrite background processing using isolates.

Closes #142.
2026-06-19 07:27:44 -07:00
Hamid Elaosta 7fc253cc88 Add adaptive Android icon support and fix app label 2026-06-18 01:18:32 +01:00
PacoX 445a18a83f Fix failed test 2026-06-16 15:01:10 +02:00
PacoX 8273c416b5 Merge pull request #5 from HDDen/update-pacox-multibyte
Updated with dev again
2026-06-16 13:38:05 +02:00
HDDen b44c69f411 Merge branch 'dev' of https://github.com/PacoX/meshcore-open into update-pacox-multibyte 2026-06-16 12:11:34 +03:00
HDDen 6492058a04 Merge branch 'dev' into update-pacox-multibyte 2026-06-16 11:53:59 +03:00
zjs81 5670ab0067 Merge pull request #468 from HDDen/test-regions
Regions: adapted to last dev and add regions fetching from repeaters (continued from PR#379)
2026-06-15 22:53:42 -07:00
zjs81 98a9197cb1 Merge remote-tracking branch 'origin/dev' into test-regions 2026-06-15 22:52:31 -07:00
zjs81 55da952bfd Merge remote-tracking branch 'origin/dev' into test-regions Also added fixes 2026-06-15 22:46:59 -07:00
zjs81 9b4742951d Merge pull request #479 from MrSurly/478-implement-right-click
Issue 478: Implement right click for desktop use
2026-06-15 21:56:13 -07:00
Zach 43f3d439ba Add jni to FFI plugin list for Linux and Windows 2026-06-15 13:12:08 -07:00
PacoX ed294decfb Merge pull request #4 from HDDen/update-pacox-multibyte
Updated to dev
2026-06-15 14:43:27 +02:00
Eric Poulsen 5f54a2cd1b dart format 2026-06-13 22:05:46 -07:00
Eric Poulsen e1536c49b1 tap --> secondaryTap, per review comments 2026-06-13 20:43:30 -07:00
Eric Poulsen bfa62523df Prefer GestureDetector over Listener for right-click in routing sheet 2026-06-13 20:43:13 -07:00
Eric Poulsen 6813a72767 Fix null-returning ternaries with if blocks, per code review comments 2026-06-13 20:32:05 -07:00
Eric Poulsen dbd3a40bdc Rename _handleMapTap to _handleMapContextPress, per review comments. 2026-06-13 20:29:36 -07:00
Eric Poulsen 3009036565 Remove white space per code review 2026-06-13 20:27:58 -07:00
Eric Poulsen 80f4fd5218 Added HitTestBehavior.opaque, per code review comments 2026-06-13 20:26:38 -07:00
Eric Poulsen 321d4b9775 Use kSecondaryMouseButton bitmask check, per code review 2026-06-13 20:23:32 -07:00
Eric Poulsen 1c183d7e67 Add right-click support to routing sheet path history rows 2026-06-13 19:46:00 -07:00
Eric Poulsen a1e6f6967c Added support for left-click copy, and snackbar message about copying the data. 2026-06-13 19:30:27 -07:00
Eric Poulsen f34c2a92c3 Add onSecondaryTap to MeshCard and fix right-click on message text bubbles 2026-06-13 19:22:16 -07:00
Eric Poulsen f1478722b0 Fix for right-click on message bubbles (right click didn't work on the actual text of the bubble) 2026-06-13 19:05:52 -07:00
Eric Poulsen c867225073 onSecondaryTap for line_of_sight_map_screen 2026-06-13 16:37:12 -07:00
Eric Poulsen 06285a02ff onSecondaryTap for map_screen 2026-06-13 16:36:55 -07:00
HDDen e92d215582 upd - fix and format 2026-06-14 01:36:49 +03:00
HDDen d61eaaaff1 Merge branch 'dev' into update-pacox-multibyte 2026-06-14 01:12:49 +03:00
ericz ecde5e1109 fix StadiaMaps Dark and add demo. 2026-06-13 20:01:43 +02:00
ericz f548f95e29 merge zjs81/dev: resolve conflicting implementations 2026-06-13 18:06:31 +02:00
HDDen 27695d372a Updated to last dev 2026-06-12 14:44:09 +03:00
HDDen d06a4c1861 Merge branch 'dev' into update-pacox-multibyte 2026-06-12 13:39:09 +03:00
PacoX c46a5ffa2f Fix direct path override validation 2026-06-05 08:34:57 +02:00
PacoX a15f5e94da fix: path formatting in DM mode 2026-06-04 20:26:46 +02:00
HDDen 9d1251f21e Merge branch 'dev' into test-regions 2026-05-31 16:52:27 +03:00
HDDen 0ca96ce5b1 Regions discovery from nearby repeaters 2026-05-31 16:47:22 +03:00
HDDen b72ade1387 Update channel_chat_screen.dart 2026-05-31 00:22:04 +03:00
HDDen 3fc038682c Merge commit 'refs/pull/379/head' of https://github.com/zjs81/meshcore-open into test-regions 2026-05-31 00:06:16 +03:00
PacoX 9e5b360f28 fix int conversion 2026-05-28 09:31:42 +02:00
PacoX 89360c8bc2 fix channels_screen 2026-05-28 09:18:42 +02:00
ericszimmermann 28df5a97e8 Merge branch 'dev' into ez_alternative_maps 2026-05-28 08:56:54 +02:00
PacoX 2e767730fd Merge branch 'dev' into dev 2026-05-28 08:51:23 +02:00
PacoX b7fe5331d0 Merge pull request #3 from HDDen/update-pacox-multibyte
Updated multibyte
2026-05-28 08:43:18 +02:00
HDDen 61fb39b9f8 Multibyte: some path fixes
- Validate encoded path hop counts before saving or sending custom paths
- Fix multi-byte path reversal by reversing hops instead of raw bytes
- Decode raw log path lengths using packed path_len format
- Localize path hash mode options and optimize map anchor lookup
2026-05-28 03:17:08 +03:00
HDDen 9e18b99e9d Multibyte: fixed path trace repeater icon and potential bugs 2026-05-28 02:52:04 +03:00
HDDen 2e4f26d7cd Multibyte: fix map 2026-05-28 02:20:56 +03:00
PacoX 6e81fdfd46 add clamp for path length 2026-05-27 08:32:16 +02:00
PacoX 5d03f99040 fix tracepath 2026-05-26 20:12:27 +02:00
PacoX e88281b7b6 fix case-sensitive prefix 2026-05-26 09:04:38 +02:00
PacoX 3a11e35a7a fix path trace overlay 2026-05-26 08:50:54 +02:00
ericz 29e551d6cb add Documentation 2026-05-25 22:24:38 +02:00
ericz 531b85b8aa add OSM Auto Map 2026-05-25 11:56:01 +02:00
PacoX 3d956596a5 Merge pull request #2 from ericszimmermann/ez_snr_multibyte2.1
Forgot the bytewise reverse of the outgoing path
2026-05-25 08:30:12 +02:00
ericz bdd0d3dee2 make raster tile source scollable 2026-05-24 11:55:50 +02:00
ericz 11a4f9a373 try OpenStreetMap Dark 2026-05-24 11:37:56 +02:00
ericz acbe4dcc49 remove inversal for outgoing path 2026-05-24 10:04:44 +02:00
ericz 9b99eadcf7 forgot the bytewise reverse of the outgoing path 2026-05-23 23:28:01 +02:00
ericz 09a83c4f22 add high dpi stadiamaps 2026-05-23 17:21:23 +02:00
PacoX 5486ae8ccb fix multibyte path in private message and repeter 2026-05-23 13:32:11 +02:00
ericz fc67d8d5aa show cached tiles 2026-05-23 13:10:59 +02:00
ericz 225d07b440 add stadiamaps.com 2026-05-23 13:10:23 +02:00
PacoX 945a118853 expose mode 3 in UI 2026-05-20 08:50:17 +02:00
PacoX 01d4f002f8 add unit tests 2026-05-19 10:50:18 +02:00
PacoX 9331b02f94 Merge pull request #1 from ericszimmermann/ez_snr_multibyte2
Multibyte Support for SNR-Indicator, channel message_bubble and Setting
2026-05-19 10:14:03 +02:00
PacoX 27f7932db9 fix import 2026-05-19 08:52:44 +02:00
PacoX 4a39a7cd46 fix from copilot review + doc fix 2026-05-19 08:49:52 +02:00
ericz 3f1bd54889 fix wrong hop count in channel_message_path_screen observed path chooser. 2026-05-18 22:49:56 +02:00
ericz 91759f531a fix double entry due to mixed 1byte and 2byte paths 2026-05-18 21:30:37 +02:00
ericz 87708e2684 fixes direct repeater mixing 1 byte paths with 2 byte Indicator, and wrong description. 2026-05-18 20:42:40 +02:00
ericz 73e5b80232 add HashMode Setting 2026-05-18 19:17:53 +02:00
ericz 28a5913ba3 multibyte for snr_indicator and map_filtering. 2026-05-18 19:00:07 +02:00
PacoX 091c754584 clean 2026-05-18 09:00:30 +02:00
PacoX cf27c59c84 path aware fix 2026-05-18 08:57:13 +02:00
PacoX 119590434a path aware 2026-05-17 20:14:16 +02:00
PacoX dc82414447 rollback-pr review 2026-05-17 19:37:43 +02:00
PacoX 23f29f2cda rollback-pr review 2026-05-17 17:44:30 +02:00
PacoX d66b16a4e8 PR review modif + packet aware 2026-05-17 15:53:00 +02:00
PacoX d6647f4701 PR review modif + packet aware 2026-05-17 15:50:25 +02:00
PacoX 87b0fd6fc7 removed distance-based rejection filter 2026-05-14 12:55:46 +02:00
PacoX 4a066e08dc fix from PR review 2026-05-13 13:44:42 +02:00
PacoX 94ed729985 bug fix 2026-05-13 10:43:57 +02:00
PacoX 8296f44a4e update doc 2026-05-13 09:09:11 +02:00
PacoX 4e70fd2a5e clean 2026-05-13 08:51:44 +02:00
PacoX ca02197414 UI fix 2026-05-13 08:49:24 +02:00
PacoX 6e2abfeb2d multi-byte paths-doc 2026-05-12 16:40:34 +02:00
PacoX b62c1c7338 multi-byte paths-debug 2026-05-12 16:33:33 +02:00
PacoX 17379394bd multi-byte paths 2026-05-12 16:08:19 +02:00
Stephan Rodemeier 0e074fd806 Add region management
This adds region management: the user can manage a list of available regions
and for each channel pick a region from that list to apply to messages.

Region discovery from nearby repeaters will be done in a separate PR.

This is a part of the work needed for #120.
2026-04-14 13:07:50 +02:00
227 changed files with 41016 additions and 1303 deletions
+3
View File
@@ -0,0 +1,3 @@
github: [zjs81]
custom:
- "https://paypal.me/zjs82"
+5 -1
View File
@@ -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 - **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 - **Node Filtering**: Filter by node type (chat, repeater, sensor) and time range
- **Location Sharing**: Share GPS coordinates and custom markers with contacts - **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 - **MGRS Coordinates**: Support for Military Grid Reference System coordinate format
### Device Management ### Device Management
@@ -224,6 +224,10 @@ Messages are transmitted as binary frames using a custom protocol optimized for
This is an open-source project. Contributions are welcome! This is an open-source project. Contributions are welcome!
## SWHID and Archive badge
[![SWH](https://archive.softwareheritage.org/badge/origin/https://github.com/zjs81/meshcore-open/)](https://archive.softwareheritage.org/browse/origin/?origin_url=https://github.com/zjs81/meshcore-open)
[![SWH](https://archive.softwareheritage.org/badge/swh:1:dir:d37a80b06359730864150ad2aeadd46cce9abd55/)](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 ### Development Guidelines
- Follow the Flutter style guide - Follow the Flutter style guide
+23 -3
View File
@@ -43,9 +43,22 @@ android {
// arguments += listOf("-DANDROID_STL=c++_shared") // arguments += listOf("-DANDROID_STL=c++_shared")
// } // }
// } // }
// ndk { // arm64-v8a only, deliberately.
// abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86_64") //
// } // * 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 { signingConfigs {
@@ -67,6 +80,13 @@ android {
} else { } else {
signingConfigs.getByName("debug") 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",
)
} }
} }
+19
View File
@@ -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.**
+1 -1
View File
@@ -22,7 +22,7 @@
<uses-feature android:name="android.hardware.usb.host" android:required="false"/> <uses-feature android:name="android.hardware.usb.host" android:required="false"/>
<application <application
android:label="meshcore_open" android:label="MeshCore Open"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"> android:icon="@mipmap/ic_launcher">
<service <service
Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

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>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

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>
+93
View File
@@ -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.
Binary file not shown.
Binary file not shown.
+93
View File
@@ -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.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 669 KiB

+2 -1
View File
@@ -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 8. [Notifications](notifications.md) - System notifications, unread badges, and notification preferences
9. [Repeater Management](repeater-management.md) - Repeater hub, status, CLI, telemetry, and neighbors 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 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 ## App Overview
+1 -1
View File
@@ -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) - 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 - Features with zero variance are automatically excluded from training
- Blends per-contact statistics with ML predictions - 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) - Observations are persisted to storage via a 2-second debounced timer (observations within 2s of app termination may be lost)
--- ---
+3
View File
@@ -120,6 +120,8 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
| 40 | CMD_GET_CUSTOM_VAR | Get custom variables | | 40 | CMD_GET_CUSTOM_VAR | Get custom variables |
| 41 | CMD_SET_CUSTOM_VAR | Set a custom variable | | 41 | CMD_SET_CUSTOM_VAR | Set a custom variable |
| 50 | CMD_SEND_BINARY_REQ | Send binary request | | 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 | | 56 | CMD_GET_STATS | Request companion radio stats |
| 57 | CMD_SEND_ANON_REQ | Send anonymous request | | 57 | CMD_SEND_ANON_REQ | Send anonymous request |
| 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration | | 58 | CMD_SET_AUTO_ADD_CONFIG | Set auto-add configuration |
@@ -162,6 +164,7 @@ On unexpected disconnection, auto-reconnect with exponential backoff:
| 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered | | 0x8A | PUSH_CODE_NEW_ADVERT | New node discovered |
| 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data | | 0x8B | PUSH_CODE_TELEMETRY_RESPONSE | Sensor telemetry data |
| 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response | | 0x8C | PUSH_CODE_BINARY_RESPONSE | Binary data response |
| 0x8E | PUSH_CODE_CONTROL_DATA | Control data push (e.g. zero-hop discovery response) |
## Data Models ## Data Models
+79
View File
@@ -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)
+5 -1
View File
@@ -52,7 +52,7 @@ These settings are sent directly to the connected device firmware.
### Radio Settings ### Radio Settings
Opens a dialog pre-populated with the device's current radio settings. Contains: Opens a dialog pre-populated with the device's current radio settings. Contains:
- **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam - **Preset dropdown**: Regional presets — selecting a preset immediately fills all fields below. Includes presets for Australia, Australia (Narrow), Australia (Mid), Australia SA WA QLD, Czech Republic, EU 433MHz, EU/UK (Long Range), EU/UK (Medium Range), EU/UK (Narrow), New Zealand, New Zealand (Narrow), Portugal 433, Portugal 869, numerous Russia city presets, Switzerland, USA Arizona, USA/Canada, and Vietnam
- **Frequency** (MHz): Free text, validated 3002500 MHz - **Frequency** (MHz): Free text, validated 3002500 MHz
- **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz) - **Bandwidth**: Dropdown (7.8 / 10.4 / 15.6 / 20.8 / 31.25 / 41.7 / 62.5 / 125 / 250 / 500 kHz)
- **Spreading Factor**: SF5SF12 - **Spreading Factor**: SF5SF12
@@ -129,6 +129,10 @@ A dedicated sub-screen for app-level preferences (nothing here is sent to the de
- **Show Other Nodes**: Toggle room/sensor markers - **Show Other Nodes**: Toggle room/sensor markers
- **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week - **Time Filter**: All time / Last 1h / Last 6h / Last 24h / Last week
- **Units**: Metric / Imperial - **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 - **Offline Map Cache**: Navigate to tile download screen
### Translation ### Translation
+16
View File
@@ -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>
-52
View File
@@ -1,73 +1,21 @@
PODS: PODS:
- Flutter (1.0.0) - Flutter (1.0.0)
- flutter_blue_plus_darwin (0.0.2):
- Flutter
- FlutterMacOS
- flutter_foreground_task (0.0.1): - flutter_foreground_task (0.0.1):
- Flutter - 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: DEPENDENCIES:
- Flutter (from `Flutter`) - 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_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: EXTERNAL SOURCES:
Flutter: Flutter:
:path: Flutter :path: Flutter
flutter_blue_plus_darwin:
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
flutter_foreground_task: flutter_foreground_task:
:path: ".symlinks/plugins/flutter_foreground_task/ios" :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: SPEC CHECKSUMS:
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
flutter_foreground_task: a159d2c2173b33699ddb3e6c2a067045d7cebb89 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 PODFILE CHECKSUM: e42b502c78c33aa1ed9d42eaea8960ce2139504b
+46 -34
View File
@@ -11,6 +11,7 @@
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 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 */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */, 9A698254711B63C3940A64CB /* libPods-Runner.a in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
@@ -85,6 +88,7 @@
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
@@ -180,13 +184,15 @@
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */, F0D7F2413C6E4B7A9B1C2D3E /* Fix Native Asset Minimum OS */,
B788CEDB957A87EE8AC593BB /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
); );
name = Runner; name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
@@ -220,6 +226,9 @@
Base, Base,
); );
mainGroup = 97C146E51CF9000F007C117D; mainGroup = 97C146E51CF9000F007C117D;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "FlutterGeneratedPluginSwiftPackage" */,
);
productRefGroup = 97C146EF1CF9000F007C117D /* Products */; productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
projectDirPath = ""; projectDirPath = "";
projectRoot = ""; projectRoot = "";
@@ -283,39 +292,6 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 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 */ = { DE3B2E091393835C0B38492E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647; 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"; 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; 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 */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -446,7 +438,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -625,7 +619,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -647,7 +643,9 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = X74VJ4UAST;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
@@ -696,6 +694,20 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* 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 */; rootObject = 97C146E61CF9000F007C117D /* Project object */;
} }
@@ -5,6 +5,24 @@
<BuildAction <BuildAction
parallelizeBuildables = "YES" parallelizeBuildables = "YES"
buildImplicitDependencies = "YES"> buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries> <BuildActionEntries>
<BuildActionEntry <BuildActionEntry
buildForTesting = "YES" buildForTesting = "YES"
+2
View File
@@ -37,6 +37,8 @@
<string>This app uses Bluetooth to communicate with MeshCore devices.</string> <string>This app uses Bluetooth to communicate with MeshCore devices.</string>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>This app uses the camera to scan QR codes for joining communities.</string> <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> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
File diff suppressed because it is too large Load Diff
+88 -2
View File
@@ -1,6 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto;
import 'package:flutter/widgets.dart'; import 'package:flutter/widgets.dart';
// Buffer Reader - sequential binary data reader with pointer tracking // Buffer Reader - sequential binary data reader with pointer tracking
@@ -206,6 +207,8 @@ const int cmdSendTelemetryReq = 39;
const int cmdGetCustomVar = 40; const int cmdGetCustomVar = 40;
const int cmdSetCustomVar = 41; const int cmdSetCustomVar = 41;
const int cmdSendBinaryReq = 50; const int cmdSendBinaryReq = 50;
const int cmdSetFloodScope = 54;
const int cmdSendControlData = 55;
const int cmdGetStats = 56; const int cmdGetStats = 56;
const int cmdSendAnonReq = 57; const int cmdSendAnonReq = 57;
const int cmdSetAutoAddConfig = 58; const int cmdSetAutoAddConfig = 58;
@@ -230,6 +233,12 @@ Uint8List buildTelemetryBinaryPayload() {
return Uint8List.fromList([reqTypeGetTelemetry, 0x00, 0x00, 0x00, 0x00]); 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 // Repeater response codes
const int respServerLoginOk = 0; const int respServerLoginOk = 0;
@@ -272,6 +281,7 @@ const int pushCodeTraceData = 0x89;
const int pushCodeNewAdvert = 0x8A; const int pushCodeNewAdvert = 0x8A;
const int pushCodeTelemetryResponse = 0x8B; const int pushCodeTelemetryResponse = 0x8B;
const int pushCodeBinaryResponse = 0x8C; const int pushCodeBinaryResponse = 0x8C;
const int pushCodeControlData = 0x8E;
// Contact/advertisement types // Contact/advertisement types
const int advTypeChat = 1; const int advTypeChat = 1;
@@ -577,9 +587,9 @@ Uint8List buildGetStatsFrame(int statsType) {
return Uint8List.fromList([cmdGetStats, statsType & 0xFF]); 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) { Uint8List buildSetPathHashModeFrame(int mode) {
final m = mode.clamp(0, 2); final m = mode.clamp(0, 3).toInt();
return Uint8List.fromList([cmdSetPathHashMode, 0, m]); return Uint8List.fromList([cmdSetPathHashMode, 0, m]);
} }
@@ -866,6 +876,67 @@ Uint8List buildSendBinaryReq(Uint8List repeaterPubKey, {Uint8List? payload}) {
return writer.toBytes(); 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 //Build a trace request frame
//[cmd][tag x4][auth x4][flag][payload] //[cmd][tag x4][auth x4][flag][payload]
Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) { Uint8List buildTraceReq(int tag, int auth, int flag, {Uint8List? payload}) {
@@ -960,3 +1031,18 @@ Uint8List buildSendTelemetryReq(Uint8List? pubKey) {
} }
return writer.toBytes(); 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]);
}
+78
View File
@@ -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,
);
}
+11 -1
View File
@@ -1,3 +1,4 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -20,6 +21,7 @@ class LinkHandler {
required String text, required String text,
required TextStyle style, required TextStyle style,
TextStyle? linkStyle, TextStyle? linkStyle,
VoidCallback? onSecondaryTap,
}) { }) {
final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style); final effectiveLinkStyle = linkStyle ?? defaultLinkStyle(context, style);
const options = LinkifyOptions(humanize: false, defaultToHttps: false); const options = LinkifyOptions(humanize: false, defaultToHttps: false);
@@ -27,7 +29,7 @@ class LinkHandler {
void onOpen(LinkableElement link) => handleLinkTap(context, link.url); void onOpen(LinkableElement link) => handleLinkTap(context, link.url);
if (PlatformInfo.isDesktop) { if (PlatformInfo.isDesktop) {
return SelectableLinkify( final linkify = SelectableLinkify(
text: text, text: text,
style: style, style: style,
linkStyle: effectiveLinkStyle, linkStyle: effectiveLinkStyle,
@@ -35,6 +37,14 @@ class LinkHandler {
linkifiers: linkifiers, linkifiers: linkifiers,
onOpen: onOpen, onOpen: onOpen,
); );
if (onSecondaryTap == null) return linkify;
return Listener(
onPointerDown: (event) {
if (event.buttons & kSecondaryMouseButton != 0) onSecondaryTap();
},
behavior: HitTestBehavior.translucent,
child: linkify,
);
} }
return Linkify( return Linkify(
text: text, text: text,
+59 -4
View File
@@ -1,5 +1,7 @@
import '../models/contact.dart'; import 'package:flutter/foundation.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../models/contact.dart';
class PathHelper { class PathHelper {
static String formatPathHex(List<int> pathBytes) { static String formatPathHex(List<int> pathBytes) {
@@ -12,10 +14,17 @@ class PathHelper {
return byte.toRadixString(16).padLeft(2, '0').toUpperCase(); 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) { static String? hopName(int byte, List<Contact> allContacts) {
final matches = allContacts final matches = allContacts
.where( .where(
(c) => (c) =>
c.publicKey.isNotEmpty &&
c.publicKey.first == byte && c.publicKey.first == byte &&
(c.type == advTypeRepeater || c.type == advTypeRoom), (c.type == advTypeRepeater || c.type == advTypeRoom),
) )
@@ -25,12 +34,58 @@ class PathHelper {
return matches.map((c) => c.name).join(' | '); 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( static String resolvePathNames(
List<int> pathBytes, List<int> pathBytes,
List<Contact> allContacts, List<Contact> allContacts,
int hashByteWidth,
) { ) {
return pathBytes if (pathBytes.isEmpty) return '';
.map((b) => hopName(b, allContacts) ?? hopHex(b))
.join(' \u2192 '); 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 ');
} }
} }
+15 -9
View File
@@ -1,6 +1,7 @@
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import 'path_helper.dart';
import '../models/contact.dart'; import '../models/contact.dart';
class PathHopResolver { class PathHopResolver {
@@ -11,30 +12,35 @@ class PathHopResolver {
required List<Contact> contacts, required List<Contact> contacts,
LatLng? endpoint, LatLng? endpoint,
bool resolveFromEnd = false, 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) { for (final contact in contacts) {
if (contact.publicKey.isEmpty) continue; if (contact.publicKey.length < width) continue;
if (contact.type != advTypeRepeater && contact.type != advTypeRoom) { if (contact.type != advTypeRepeater && contact.type != advTypeRoom) {
continue; continue;
} }
candidatesByPrefix final prefix = PathHelper.formatHopHex(
.putIfAbsent(contact.publicKey.first, () => <Contact>[]) contact.publicKey.sublist(0, width),
.add(contact); );
candidatesByPrefix.putIfAbsent(prefix, () => <Contact>[]).add(contact);
} }
for (final candidates in candidatesByPrefix.values) { for (final candidates in candidatesByPrefix.values) {
candidates.sort((a, b) => b.lastSeen.compareTo(a.lastSeen)); 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 final indexes = resolveFromEnd
? List<int>.generate(pathBytes.length, (i) => pathBytes.length - 1 - i) ? List<int>.generate(hops.length, (i) => hops.length - 1 - i)
: List<int>.generate(pathBytes.length, (i) => i); : List<int>.generate(hops.length, (i) => i);
final distance = Distance(); final distance = Distance();
var previousPosition = endpoint; var previousPosition = endpoint;
for (final index in indexes) { for (final index in indexes) {
final candidates = candidatesByPrefix[pathBytes[index]]; final candidates =
candidatesByPrefix[PathHelper.formatHopHex(hops[index])];
if (candidates == null || candidates.isEmpty) continue; if (candidates == null || candidates.isEmpty) continue;
var bestIndex = 0; var bestIndex = 0;
+288 -1
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Невалидна ширина или дължина.", "settings_locationInvalid": "Невалидна ширина или дължина.",
"settings_latitude": "Широчина", "settings_latitude": "Широчина",
"settings_longitude": "Дължина", "settings_longitude": "Дължина",
"settings_autoZeroHopAdvertOnGpsUpdate": "Автоматична zero-hop обява при GPS обновяване",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).",
"settings_privacyMode": "Режим на поверителност", "settings_privacyMode": "Режим на поверителност",
"settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите", "settings_privacyModeSubtitle": "Скриване на име/местоположение в рекламите",
"settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.", "settings_privacyModeToggle": "Активирайте режим на поверителност, за да скриете името и местоположението си в рекламите.",
@@ -240,6 +242,19 @@
"appSettings_last6Hours": "Последни 6 часа", "appSettings_last6Hours": "Последни 6 часа",
"appSettings_last24Hours": "Последно 24 часа", "appSettings_last24Hours": "Последно 24 часа",
"appSettings_lastWeek": "Миналата седмица", "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_offlineMapCache": "Кеш на офлайн карти",
"appSettings_noAreaSelected": "Няма избрана област", "appSettings_noAreaSelected": "Няма избрана област",
"appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Избрана е област (мащаб {minZoom}-{maxZoom})",
@@ -769,6 +784,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}", "mapCache_boundsLabel": "Север {north}, Юг {south}, Изток {east}, Запад {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Без посочено местоположение", "pathMap_noLocation": "Без посочено местоположение",
"pathMap_followPacket": "Проследи пакета", "pathMap_followPacket": "Проследи пакета",
"pathMap_unfollowPacket": "Спри проследяването на пакета", "pathMap_unfollowPacket": "Спри проследяването на пакета",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Региони",
"settings_regionSettingsSubtitle": "Управляне на хранилищни региони",
"settings_regionManagement_screenTitle": "Регионално управление",
"settings_regionNameHint": "Възразна название на региона",
"settings_regionAddRegion": "Добавете регион",
"settings_regionFetchRegions": "Извън рептерите от региони",
"settings_regionFetchRegionsFail": "Не бъде открити никако региони",
"settings_regionFetchRegionsAlreadyExists": "Тата регион е уже добавена",
"settings_regionName": "Регионноименоване",
"settings_regionDeleted": "Регион отстранен",
"settings_deleteRegion": "Удаляне на регион",
"settings_deleteRegionConfirm": "Състрясна: отпиште \"{region}\" от списка региони?",
"settings_infoHardware": "Оборудование",
"settings_infoFirmware": "Програмно-оборонни системни программи (фirmware)",
"repeater_pathHashModeOption0": "0 - 1 байт",
"repeater_pathHashModeOption1": "1 - 2 байта",
"repeater_pathHashModeOption2": "2 - 3 байта",
"repeater_pathHashModeOption3": "3 - 4 байта",
"appSettings_batteryLipoHv": "LiPo ВЗГ (3,0-4,35 В)",
"channels_regionSetTo": "Регион: {region}",
"channels_regionNotSet": "Регион: ние",
"channels_regionSelect_Title": "Выберете регион",
"channels_clearRegion": "Чиста зона",
"chat_sendImage": "Изпрати изображение",
"chat_imagePickFailed": "Не мог да откривам та изображин",
"chat_receivedGif": "Получил GIF",
"repeater_keySettings": "Изменяне на идентичности ключа",
"repeater_keySettingsSubtitle": "Замени ключовият пар от публични/частни",
"repeater_prvKey": "Частни ключ",
"repeater_prvKeyHelper": "Нова приватна ключ на повторчето, 128-символовна шестнадатерична строка.",
"repeater_generatePrvKey": "Генериране на случайна ключи",
"repeater_stopGeneratingPrvKey": "Прервана поискна на ключова парта",
"repeater_pubKey": "Публична ключ",
"repeater_pubKeyHelper": "Та е публичната ключ, която се свързува с генериранта приватна ключ. Трябва да не настройте тоа напрямо.",
"repeater_pubKeyPrefix": "Желата префикс",
"repeater_pubKeyPrefixHelper": "Наискате публична ключ, който почева с този шестнадатницил. Ожидавани проби: {tries}.",
"imageMessages_enableTitle": "Абilitiraм изображените сообщения",
"imageMessages_enableSubtitle": "Изправате изображения чрез сетката. Треба е одноразовна загрузка модел на изображение.",
"imageMessages_modelSectionTitle": "Изображени модель",
"imageMessages_downloadModel": "Скачане",
"imageMessages_cancelDownload": "Отмена",
"imageMessages_removeModel": "Удалете модель",
"imageMessages_modelReady": "Готов",
"imageMessages_modelNotPublished": "Еще не опубликовано — тази версия не може да скане.",
"imageMessages_downloadFailed": "Модел за изображение не може да бъде скачан.",
"imageMessages_autoProcessTitle": "Автоматически обрабатвате изображения",
"imageMessages_autoProcessSubtitle": "Катализиране на всята изображение сразу по пришествие. Използване на около 2 ГБ на втора пъти; откажете от реконструкцията чаке при натискане.",
"imageSend_title": "Изпрати изображение",
"imageSend_cropNote": "Масштабиран до 512 × 512 · соотношение сторон не съхранясе",
"imageSend_originalSize": "Късно на използване на език:",
"imageSend_onAirSize": "В ефир",
"imageSend_quality": "Качество",
"imageSend_qualityStandard": "Стандарт",
"imageSend_qualityHigh": "Высок",
"imageSend_packetsLabel": "Пакети",
"imageSend_airtimeLabel": "Времето на изкъсване",
"imageSend_sizeLabel": "Баланса",
"imageSend_packetsCount": "{count} {count, plural, =1{пакет} other{пакета}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Настройки на радио неизвестни",
"imageSend_radioUnknownBody": "Усредете се с устройство, за да се отработа часът на изстъпане.",
"imageSend_longSendTitle": "Долга пресилка",
"imageSend_longSendBody": "Тази буде поддержива открит отряд на около {duration}.",
"imageSend_floodNote": "Руйжинг на превъште: всички ретранслятори в диапазонта превратят всички пакети, тъйа каналят оставря дълше, от този час.",
"imageSend_parityTitle": "Возобновителен пакет",
"imageSend_paritySubtitle": "Едно дополително пакет. Групни извещания не принимаются, след това разбирателят едно пакетто, какщо е пропущен един пакет.",
"imageSend_send": "Отправляй",
"imageSend_cancel": "Отмена",
"imageSend_encodeFailed": "Тази изображение не може да бъде кодирано.",
"imageSend_codecDownloading": "Модел на изображение еще разгрузира.",
"imageSend_codecUnavailable": "Изглед отправа не е доступен на този устройство.",
"imageSend_codecDisabled": "Изображенията на извещенията е отключена в настройките.",
"imageSend_deviceUnsupported": "Та радиота не може да пресылава изображени пакети. Подключите устройство, който работи с компанионски прошивка 13 или новия.",
"imageSend_directMessagesUnsupported": "Изображенията се превозводя се как групова информация, следато да се отправят единствено на канал — не в лична вестка.",
"imageSend_tooLarge": "Таата изображение е кодирано в по-числа пакети, отгоре както формат на сетта разрешава.",
"imageSend_sentConfirmation": "Изображението е изпратено като {count} {count, plural, =1{пакет} other{пакета}}.",
"imageSend_sendFailed": "Изображение не може да бъде отправено: {error}",
"imageSend_sendingProgress": "Отправка наизображение — пакет {sent} от {total}",
"receivedImage_senderPrefix": "Узъдето {prefix}",
"receivedImage_incoming": "{received} от {total} пакета",
"receivedImage_queued": "Ожидането за декодиране",
"receivedImage_tapToDecode": "Натискнете за декодиране",
"receivedImage_decoding": "Повторно… за приблизително 1 секунда",
"receivedImage_incomplete": "Изображение неполно — {received} от {total} пакети отправили",
"receivedImage_corrupt": "Изображение не може да бъде воссоздано",
"receivedImage_decoderMissing": "Получена изображение — разгадка на изображение е вреjan",
"receivedImage_evicted": "Изображение не е хранитено",
"receivedImage_retry": "Повтори",
"receivedImage_decodeAgain": "Правилейте заново",
"receivedImage_openSettings": "Настройте",
"receivedImage_tapToProcess": "Натискнете за обработка",
"receivedImage_awaiting": "{bytes} байта · {packets} {packets, plural, =1{пакет} other{пакета}}",
"imageSend_secondsValue": "{seconds} секунди",
"imageSend_minutesSecondsValue": "{minutes} мин {seconds} сек"
} }
+288 -1
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.", "settings_locationInvalid": "Ungültiger Breiten- oder Längengrad.",
"settings_latitude": "Breitengrad", "settings_latitude": "Breitengrad",
"settings_longitude": "Längengrad", "settings_longitude": "Längengrad",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatische Zero-Hop-Ankündigung bei GPS-Update",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).",
"settings_privacyMode": "Privatsphärenmodus", "settings_privacyMode": "Privatsphärenmodus",
"settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen", "settings_privacyModeSubtitle": "Name und Standort in Ankündigungen verbergen",
"settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.", "settings_privacyModeToggle": "Privatsphärenmodus aktivieren, um Namen und Standort in Ankündigungen zu verbergen.",
@@ -240,6 +242,19 @@
"appSettings_last6Hours": "Letzte 6 Stunden", "appSettings_last6Hours": "Letzte 6 Stunden",
"appSettings_last24Hours": "Letzte 24 Stunden", "appSettings_last24Hours": "Letzte 24 Stunden",
"appSettings_lastWeek": "Letzte Woche", "appSettings_lastWeek": "Letzte Woche",
"appSettings_rasterTileSource": "Quelle für Rasterkacheln",
"appSettings_stadiaEndpoint": "Stadia-Endpunkt",
"appSettings_stadiaApiKey": "Stadia-API-Schlüssel",
"appSettings_stadiaApiKeyRequired": "Für die Nutzung von Stadia Maps erforderlich",
"appSettings_stadiaApiKeyConfigured": "Konfiguriert: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.",
"appSettings_offlineMapCache": "Offline-Karten-Cache", "appSettings_offlineMapCache": "Offline-Karten-Cache",
"appSettings_noAreaSelected": "Kein Bereich ausgewählt", "appSettings_noAreaSelected": "Kein Bereich ausgewählt",
"appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Bereich ausgewählt (Zoom {minZoom}-{maxZoom})",
@@ -791,6 +806,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2488,6 +2553,13 @@
"settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware", "settings_companionDebugLog": "Debug-Protokoll für die Begleitsoftware",
"settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten", "settings_companionDebugLogSubtitle": "BLE/TCP/USB-Befehle, Antworten und Rohdaten",
"repeater_chanUtil": "Nutzung des Kanals", "repeater_chanUtil": "Nutzung des Kanals",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@routing_lastWorked": { "@routing_lastWorked": {
"placeholders": { "placeholders": {
"when": { "when": {
@@ -2495,6 +2567,27 @@
} }
} }
}, },
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"settings_regionSettings": "Regionen",
"settings_regionSettingsSubtitle": "Gespeicherte Regionen verwalten",
"settings_regionManagement_screenTitle": "Regions-Verwaltung",
"settings_regionNameHint": "Regions-Namen eingeben",
"settings_regionAddRegion": "Region hinzufügen",
"settings_regionDeleted": "Region entfernt",
"settings_regionName": "Regions-Name",
"settings_deleteRegion": "Region entfernen",
"settings_deleteRegionConfirm": "Region \"{region}\" aus der Liste entfernen?",
"channels_regionNotSet": "Region: keine",
"channels_regionSetTo": "Region: {region}",
"channels_regionSelect_Title": "Region auswählen",
"channels_clearRegion": "Region zurücksetzen",
"@routing_deliveryCounts": { "@routing_deliveryCounts": {
"placeholders": { "placeholders": {
"successes": { "successes": {
@@ -2674,5 +2767,199 @@
"pathMap_noLocation": "Keine Standortdaten", "pathMap_noLocation": "Keine Standortdaten",
"pathMap_followPacket": "Ansicht auf Paket fixieren", "pathMap_followPacket": "Ansicht auf Paket fixieren",
"pathMap_unfollowPacket": "Fixierung aufheben", "pathMap_unfollowPacket": "Fixierung aufheben",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionFetchRegions": "Regionen von Wiederholern abrufen",
"settings_regionFetchRegionsFail": "Es wurden keine Regionen gefunden.",
"settings_regionFetchRegionsAlreadyExists": "Diese Region wurde bereits hinzugefügt",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 Byte",
"repeater_pathHashModeOption1": "1 - 2 Bytes",
"repeater_pathHashModeOption2": "2 - 3 Bytes",
"repeater_pathHashModeOption3": "3 - 4 Bytes",
"appSettings_batteryLipoHv": "LiPo HV (3,04,35 V)",
"chat_sendImage": "Bild senden",
"chat_imagePickFailed": "Konnte dieses Bild nicht öffnen.",
"chat_receivedGif": "Erhalten eines GIF",
"repeater_keySettings": "Identitätsschlüssel ändern",
"repeater_keySettingsSubtitle": "Ändern Sie das öffentliche/private Schlüsselpaar",
"repeater_prvKey": "Privatschlüssel",
"repeater_prvKeyHelper": "Ein neuer privater Schlüssel für den Repeater, eine 128-stellige Hexadezimalzeichenkette.",
"repeater_generatePrvKey": "Generiere ein zufälliges Schlüsselpaar",
"repeater_stopGeneratingPrvKey": "Suchvorgang für Schlüsselpaar unterbrechen",
"repeater_pubKey": "Öffentlicher Schlüssel",
"repeater_pubKeyHelper": "Dies ist der öffentliche Schlüssel, der zum generierten privaten Schlüssel gehört. Sie können dies nicht direkt festlegen.",
"repeater_pubKeyPrefix": "Gewünschtes Präfix",
"repeater_pubKeyPrefixHelper": "Suchen Sie einen öffentlichen Schlüssel, der mit diesen Hexadezimalziffern beginnt. Erwartete Versuche: {tries}.",
"imageMessages_enableTitle": "Bildnachrichten aktivieren",
"imageMessages_enableSubtitle": "Bilder über das Netzwerk senden. Einmaliges Herunterladen eines Bildmodells erforderlich.",
"imageMessages_modelSectionTitle": "Bildmodell",
"imageMessages_downloadModel": "Herunterladen",
"imageMessages_cancelDownload": "Stornieren",
"imageMessages_removeModel": "Modell entfernen",
"imageMessages_modelReady": "Bereit",
"imageMessages_modelNotPublished": "Noch nicht veröffentlicht dieser Build kann es nicht herunterladen.",
"imageMessages_downloadFailed": "Das Bildmodell konnte nicht heruntergeladen werden.",
"imageMessages_autoProcessTitle": "Automatisch Bilder verarbeiten",
"imageMessages_autoProcessSubtitle": "Rekonstruiere jedes Bild sofort nach dem Eintreffen. Verbraucht etwa 2 GB Arbeitsspeicher für jede zweite Wiederherstellung; deaktiviere die automatische Rekonstruktion mit einem Tippen.",
"imageSend_title": "Bild senden",
"imageSend_cropNote": "Für 512 × 512 skaliert · Seitenverhältnis nicht erhalten",
"imageSend_originalSize": "Originaltext:\n\nThe quick brown fox jumps over the lazy dog.\n\nÜbersetzung:\n\nDer schnelle braune Fuchs springt über den faulen Hund.",
"imageSend_onAirSize": "On air",
"imageSend_quality": "Qualität",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Hoch",
"imageSend_packetsLabel": "Pakete",
"imageSend_airtimeLabel": "Sendezeit",
"imageSend_sizeLabel": "Nutzlast",
"imageSend_packetsCount": "{count} {count, plural, =1{Paket} other{Pakete}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radioeinstellungen unbekannt",
"imageSend_radioUnknownBody": "Verbinden Sie das Gerät, damit die Sendezeit berechnet werden kann.",
"imageSend_longSendTitle": "Lange Übertragung",
"imageSend_longSendBody": "Dies hält den Kanal für etwa {duration}.",
"imageSend_floodNote": "Flutweiterleitung: Jeder Repeater im Bereich leitet jedes Paket weiter, wodurch der Kanal länger als dieser Zeitraum beschäftigt bleibt.",
"imageSend_parityTitle": "Wiederherstellungspaket",
"imageSend_paritySubtitle": "Ein zusätzliches Paket. Gruppen-Nachrichten werden nicht bestätigt, daher kann der Empfänger das Bild rekonstruieren, falls ein Paket verloren geht.",
"imageSend_send": "Senden",
"imageSend_cancel": "Stornieren",
"imageSend_encodeFailed": "Dieses Bild konnte nicht kodiert werden.",
"imageSend_codecDownloading": "Das Bildmodell wird noch heruntergeladen.",
"imageSend_codecUnavailable": "Bildversand ist auf diesem Gerät nicht verfügbar.",
"imageSend_codecDisabled": "Bildnachrichten sind in den Einstellungen deaktiviert.",
"imageSend_deviceUnsupported": "Dieses Radio kann keine Bildpakete senden. Verbinden Sie ein Gerät mit der Companion-Firmware-Version 13 oder neuer.",
"imageSend_directMessagesUnsupported": "Bilder werden als Gruppendaten übertragen, daher können sie nur an einen Kanal gesendet werden nicht per Direktnachricht.",
"imageSend_tooLarge": "Dieses Bild wurde in mehr Pakete kodiert, als das Mesh-Format zulässt.",
"imageSend_sentConfirmation": "Bild gesendet als {count} {count, plural, =1{Paket} other{Pakete}}.",
"imageSend_sendFailed": "Das Bild konnte nicht gesendet werden: {error}",
"imageSend_sendingProgress": "Bildversand — Paket {sent} von {total}",
"receivedImage_senderPrefix": "Kritisch: Diese Platzhalter {prefix} beibehalten.",
"receivedImage_incoming": "{received} von {total} Paketen",
"receivedImage_queued": "Warten auf die Entschlüsselung",
"receivedImage_tapToDecode": "Tippen Sie, um zu entschlüsseln",
"receivedImage_decoding": "Rekonstruktion… ca. 1 s",
"receivedImage_incomplete": "Bild unvollständig — {received} von {total} Paketen eingetroffen",
"receivedImage_corrupt": "Das Bild konnte nicht rekonstruiert werden.",
"receivedImage_decoderMissing": "Bild empfangen — Bilddekodierung fehlgeschlagen",
"receivedImage_evicted": "Bild nicht mehr gespeichert",
"receivedImage_retry": "Versuchen Sie es erneut",
"receivedImage_decodeAgain": "Entschlüsseln Sie es erneut",
"receivedImage_openSettings": "Einrichten",
"receivedImage_tapToProcess": "Klicken Sie, um fortzufahren",
"receivedImage_awaiting": "{bytes} Bytes · {packets} {packets, plural, =1{Paket} other{Pakete}}",
"imageSend_secondsValue": "{seconds} s",
"imageSend_minutesSecondsValue": "{minutes} Min {seconds} Sek"
} }
+290 -3
View File
@@ -158,6 +158,25 @@
"settings_radioSettings": "Radio Settings", "settings_radioSettings": "Radio Settings",
"settings_radioSettingsSubtitle": "Frequency, power, spreading factor", "settings_radioSettingsSubtitle": "Frequency, power, spreading factor",
"settings_radioSettingsUpdated": "Radio settings updated", "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_location": "Location",
"settings_locationSubtitle": "GPS coordinates", "settings_locationSubtitle": "GPS coordinates",
"settings_locationUpdated": "Location and GPS settings updated", "settings_locationUpdated": "Location and GPS settings updated",
@@ -187,6 +206,8 @@
"settings_telemetryEnvironmentMode": "Telemetry Environment Mode", "settings_telemetryEnvironmentMode": "Telemetry Environment Mode",
"settings_advertLocation": "Advert Location", "settings_advertLocation": "Advert Location",
"settings_advertLocationSubtitle": "Include location in advert.", "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_multiAck": "Multi-ACKs",
"settings_telemetryModeUpdated": "Telemetry mode updated", "settings_telemetryModeUpdated": "Telemetry mode updated",
"settings_actions": "Actions", "settings_actions": "Actions",
@@ -227,6 +248,8 @@
"settings_infoPublicKey": "Public Key", "settings_infoPublicKey": "Public Key",
"settings_infoContactsCount": "Contacts Count", "settings_infoContactsCount": "Contacts Count",
"settings_infoChannelCount": "Channel Count", "settings_infoChannelCount": "Channel Count",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"settings_presets": "Presets", "settings_presets": "Presets",
"settings_frequency": "Frequency (MHz)", "settings_frequency": "Frequency (MHz)",
"settings_frequencyHelper": "300.0 - 2500.0", "settings_frequencyHelper": "300.0 - 2500.0",
@@ -271,6 +294,10 @@
"appSettings_languageBg": "Български", "appSettings_languageBg": "Български",
"appSettings_languageRu": "Русский", "appSettings_languageRu": "Русский",
"appSettings_languageUk": "Українська", "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_enableMessageTracing": "Enable Message Tracing",
"appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages", "appSettings_enableMessageTracingSubtitle": "Show detailed routing and timing metadata for messages",
"appSettings_notifications": "Notifications", "appSettings_notifications": "Notifications",
@@ -318,6 +345,7 @@
"appSettings_batteryNmc": "18650 NMC (3.0-4.2V)", "appSettings_batteryNmc": "18650 NMC (3.0-4.2V)",
"appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)", "appSettings_batteryLifepo4": "LiFePO4 (2.6-3.65V)",
"appSettings_batteryLipo": "LiPo (3.0-4.2V)", "appSettings_batteryLipo": "LiPo (3.0-4.2V)",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"appSettings_mapDisplay": "Map Display", "appSettings_mapDisplay": "Map Display",
"appSettings_showRepeaters": "Show Repeaters", "appSettings_showRepeaters": "Show Repeaters",
"appSettings_showRepeatersSubtitle": "Display repeater nodes on the map", "appSettings_showRepeatersSubtitle": "Display repeater nodes on the map",
@@ -342,6 +370,19 @@
"appSettings_last6Hours": "Last 6 hours", "appSettings_last6Hours": "Last 6 hours",
"appSettings_last24Hours": "Last 24 hours", "appSettings_last24Hours": "Last 24 hours",
"appSettings_lastWeek": "Last week", "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_offlineMapCache": "Offline Map Cache",
"appSettings_unitsTitle": "Units", "appSettings_unitsTitle": "Units",
"appSettings_unitsMetric": "Metric (m / km)", "appSettings_unitsMetric": "Metric (m / km)",
@@ -635,6 +676,18 @@
"channels_scanQrCodeComingSoon": "Coming soon", "channels_scanQrCodeComingSoon": "Coming soon",
"channels_enterHashtag": "Enter hashtag", "channels_enterHashtag": "Enter hashtag",
"channels_hashtagHint": "e.g. #team", "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_noMessages": "No messages yet",
"chat_sendMessage": "Send message", "chat_sendMessage": "Send message",
"chat_sendMessageTo": "Send a message to {contactName}", "chat_sendMessageTo": "Send a message to {contactName}",
@@ -688,6 +741,12 @@
} }
}, },
"chat_sendGif": "Send GIF", "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_reply": "Reply",
"chat_addReaction": "Add Reaction", "chat_addReaction": "Add Reaction",
"chat_me": "Me", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1377,7 +1486,24 @@
"repeater_advancedSettings": "Advanced", "repeater_advancedSettings": "Advanced",
"repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators", "repeater_advancedSettingsSubtitle": "Tuning knobs for experienced operators",
"repeater_pathHashMode": "Path hash mode", "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_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_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", "repeater_directTxDelay": "Direct TX delay",
@@ -1558,7 +1684,7 @@
"repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).", "repeater_cliHelpPowersavingOnOff": "Enables or disables powersaving mode (where supported).",
"repeater_cliHelpErase": "(Serial only) Formats the device file system. Wipes all settings and contacts.", "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_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_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_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.", "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_settingsTile": "Radio stats",
"radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime", "radioStats_settingsSubtitle": "Noise floor, RSSI, SNR, and airtime",
"translation_title": "Translation", "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_enableTitle": "Enable translation",
"translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.", "translation_enableSubtitle": "Translate incoming messages and allow pre-send translation.",
"translation_composerTitle": "Translate before sending", "translation_composerTitle": "Translate before sending",
@@ -2595,5 +2732,155 @@
"pathMap_expandPanel": "Expand panel", "pathMap_expandPanel": "Expand panel",
"pathMap_noLocation": "No location", "pathMap_noLocation": "No location",
"pathMap_followPacket": "Lock view to packet", "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"
}
}
}
} }
+288 -1
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitud o longitud inválidos.", "settings_locationInvalid": "Latitud o longitud inválidos.",
"settings_latitude": "Latitud", "settings_latitude": "Latitud",
"settings_longitude": "Longitud", "settings_longitude": "Longitud",
"settings_autoZeroHopAdvertOnGpsUpdate": "Anuncio de cero saltos automático al actualizar GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).",
"settings_privacyMode": "Modo de privacidad", "settings_privacyMode": "Modo de privacidad",
"settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios", "settings_privacyModeSubtitle": "Ocultar nombre/ubicación en anuncios",
"settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.", "settings_privacyModeToggle": "Activar el modo de privacidad para ocultar tu nombre y ubicación en los anuncios.",
@@ -240,6 +242,19 @@
"appSettings_last6Hours": "Últimas 6 horas", "appSettings_last6Hours": "Últimas 6 horas",
"appSettings_last24Hours": "Últimas 24 horas", "appSettings_last24Hours": "Últimas 24 horas",
"appSettings_lastWeek": "La semana pasada", "appSettings_lastWeek": "La semana pasada",
"appSettings_rasterTileSource": "Fuente de teselas ráster",
"appSettings_stadiaEndpoint": "Punto de acceso de Stadia",
"appSettings_stadiaApiKey": "Clave API de Stadia",
"appSettings_stadiaApiKeyRequired": "Obligatorio para usar Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Configurado: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.",
"appSettings_offlineMapCache": "Caché de mapa sin conexión", "appSettings_offlineMapCache": "Caché de mapa sin conexión",
"appSettings_noAreaSelected": "No se ha seleccionado ningún área", "appSettings_noAreaSelected": "No se ha seleccionado ningún área",
"appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Área seleccionada (zoom {minZoom}-{maxZoom})",
@@ -791,6 +806,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2674,5 +2739,227 @@
"pathMap_noLocation": "Sin ubicación", "pathMap_noLocation": "Sin ubicación",
"pathMap_followPacket": "Seguir paquete", "pathMap_followPacket": "Seguir paquete",
"pathMap_unfollowPacket": "Dejar de seguir el paquete", "pathMap_unfollowPacket": "Dejar de seguir el paquete",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiones",
"settings_regionSettingsSubtitle": "Gestionar regiones almacenadas",
"settings_regionManagement_screenTitle": "Gestión de Regiones",
"settings_regionNameHint": "Nombre de la región",
"settings_regionAddRegion": "Añadir región",
"settings_regionFetchRegions": "Obtener regiones de repetidores",
"settings_regionFetchRegionsFail": "No se encontraron regiones",
"settings_regionFetchRegionsAlreadyExists": "Esta región ya ha sido agregada",
"settings_regionName": "Nombre de la Región",
"settings_regionDeleted": "Región eliminada",
"settings_deleteRegion": "Eliminar Región",
"settings_deleteRegionConfirm": "¿Eliminar \"{region}\" de la lista de regiones?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"channels_regionSetTo": "Región: {region}",
"channels_regionNotSet": "Región: ninguna",
"channels_regionSelect_Title": "Seleccione una región",
"channels_clearRegion": "Zona despejada",
"chat_sendImage": "Enviar imagen",
"chat_imagePickFailed": "No pude abrir esa imagen",
"chat_receivedGif": "Recibido un GIF",
"repeater_keySettings": "Cambiar Claves de Identidad",
"repeater_keySettingsSubtitle": "Cambiar la pareja de claves pública/privada",
"repeater_prvKey": "Clave privada",
"repeater_prvKeyHelper": "Una nueva clave privada para el repetidor, una cadena hexadecimal de 128 caracteres.",
"repeater_generatePrvKey": "Genera un par de claves aleatorias",
"repeater_stopGeneratingPrvKey": "Interrumpir la búsqueda de par de claves",
"repeater_pubKey": "Clave pública",
"repeater_pubKeyHelper": "Esta es la clave pública que corresponde a la clave privada generada. No se puede establecer directamente.",
"repeater_pubKeyPrefix": "Prefijo deseado",
"repeater_pubKeyPrefixHelper": "Encuentre una clave pública que comience con estos dígitos hexadecimales. Se esperan {tries} intentos.",
"imageMessages_enableTitle": "Mensajes de imagen",
"imageMessages_enableSubtitle": "Enviar imágenes a través de la malla. Requiere la descarga de un modelo de imagen único.",
"imageMessages_modelSectionTitle": "Modelo de imagen",
"imageMessages_downloadModel": "Descargar",
"imageMessages_cancelDownload": "Cancelar",
"imageMessages_removeModel": "Eliminar modelo",
"imageMessages_modelReady": "Listo",
"imageMessages_modelNotPublished": "Aún no publicado — esta versión no puede descargarlo.",
"imageMessages_downloadFailed": "El modelo de imagen no pudo ser descargado.",
"imageMessages_autoProcessTitle": "Procesar imágenes automáticamente",
"imageMessages_autoProcessSubtitle": "Reconstruya cada imagen tan pronto como llegue. Utiliza aproximadamente 2 GB de memoria durante un segundo cada vez; desactívelo para reconstruir con un toque.",
"imageSend_title": "Enviar imagen",
"imageSend_cropNote": "Redimensionado a 512 × 512 · la relación de aspecto no se conservó",
"imageSend_originalSize": "Texto original",
"imageSend_onAirSize": "En emisión",
"imageSend_quality": "Calidad",
"imageSend_qualityStandard": "Estándar",
"imageSend_qualityHigh": "Alto",
"imageSend_packetsLabel": "Paquetes",
"imageSend_airtimeLabel": "Tiempo en pantalla",
"imageSend_sizeLabel": "Carga útil",
"imageSend_packetsCount": "{count} {count, plural, =1{paquete} other{paquetes}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Configuración de radio desconocida",
"imageSend_radioUnknownBody": "Conéctese a un dispositivo para que se pueda calcular la hora de transmisión.",
"imageSend_longSendTitle": "Larga transmisión",
"imageSend_longSendBody": "Esto mantendrá el canal durante aproximadamente {duration}.",
"imageSend_floodNote": "Enrutamiento de inundación: cada repetidor dentro del rango retransmite cada paquete, por lo que el canal permanece ocupado durante más tiempo que esto.",
"imageSend_parityTitle": "Paquete de recuperación",
"imageSend_paritySubtitle": "Un paquete adicional. Los mensajes grupales no son reconocidos, por lo que esto permite al receptor reconstruir la imagen si se pierde un solo paquete.",
"imageSend_send": "Enviar",
"imageSend_cancel": "Cancelar",
"imageSend_encodeFailed": "Esta imagen no pudo ser codificada.",
"imageSend_codecDownloading": "El modelo de imagen todavía se está descargando.",
"imageSend_codecUnavailable": "El envío de imágenes no está disponible en este dispositivo.",
"imageSend_codecDisabled": "Los mensajes de imagen están desactivados en la configuración.",
"imageSend_deviceUnsupported": "Esta radio no puede enviar paquetes de imagen. Conecte un dispositivo con el firmware Companion versión 13 o superior.",
"imageSend_directMessagesUnsupported": "Las imágenes se transmiten como datos grupales, por lo que solo se pueden enviar a un canal, no mediante mensajes directos.",
"imageSend_tooLarge": "Esa imagen se codificó en más paquetes de los que permite el formato de malla.",
"imageSend_sentConfirmation": "Imagen enviada como {count} {count, plural, =1{paquete} other{paquetes}}.",
"imageSend_sendFailed": "La imagen no pudo ser enviada: {error}",
"imageSend_sendingProgress": "Envío de imagen — paquete {sent} de {total}",
"receivedImage_senderPrefix": "Nodo {prefix}",
"receivedImage_incoming": "{received} de {total} paquetes",
"receivedImage_queued": "Esperando descifrar",
"receivedImage_tapToDecode": "Toque para decodificar",
"receivedImage_decoding": "Reconstruyendo… aproximadamente 1 segundo",
"receivedImage_incomplete": "Imagen incompleta — {received} de {total} paquetes llegados",
"receivedImage_corrupt": "La imagen no pudo ser reconstruida",
"receivedImage_decoderMissing": "Imagen recibida — la decodificación de la imagen no funciona",
"receivedImage_evicted": "Imagen ya no almacenada",
"receivedImage_retry": "Inténtalo de nuevo",
"receivedImage_decodeAgain": "Decodificar de nuevo",
"receivedImage_openSettings": "Configurar",
"receivedImage_tapToProcess": "Toque para procesar",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{paquete} other{paquetes}}",
"imageSend_secondsValue": "{seconds} segundos",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+288 -1
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitude ou longitude invalide.", "settings_locationInvalid": "Latitude ou longitude invalide.",
"settings_latitude": "Latitude", "settings_latitude": "Latitude",
"settings_longitude": "Longitude", "settings_longitude": "Longitude",
"settings_autoZeroHopAdvertOnGpsUpdate": "Annonce zéro saut automatique lors de la mise à jour GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l'annonce).",
"settings_privacyMode": "Mode de confidentialité", "settings_privacyMode": "Mode de confidentialité",
"settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces", "settings_privacyModeSubtitle": "Masquer le nom et la localisation dans les annonces",
"settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.", "settings_privacyModeToggle": "Activez le mode confidentialité pour masquer votre nom et votre localisation dans les annonces.",
@@ -240,6 +242,19 @@
"appSettings_last6Hours": "Dernières 6 heures", "appSettings_last6Hours": "Dernières 6 heures",
"appSettings_last24Hours": "Dernières 24 heures", "appSettings_last24Hours": "Dernières 24 heures",
"appSettings_lastWeek": "La semaine dernière", "appSettings_lastWeek": "La semaine dernière",
"appSettings_rasterTileSource": "Source de tuiles raster",
"appSettings_stadiaEndpoint": "Point de terminaison Stadia",
"appSettings_stadiaApiKey": "Clé API Stadia",
"appSettings_stadiaApiKeyRequired": "Requis pour lutilisation de Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Configuré : {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Entrez votre clé API Stadia Maps. Cette application lutilise pour les requêtes de tuiles raster.",
"appSettings_offlineMapCache": "Cache de carte hors ligne", "appSettings_offlineMapCache": "Cache de carte hors ligne",
"appSettings_noAreaSelected": "Aucune zone sélectionnée", "appSettings_noAreaSelected": "Aucune zone sélectionnée",
"appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Zone sélectionnée (zoom {minZoom}-{maxZoom})",
@@ -791,6 +806,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, O {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2674,5 +2739,227 @@
"pathMap_noLocation": "Aucune localisation", "pathMap_noLocation": "Aucune localisation",
"pathMap_followPacket": "Verrouiller la vue sur le paquet", "pathMap_followPacket": "Verrouiller la vue sur le paquet",
"pathMap_unfollowPacket": "Déverrouiller la vue du paquet", "pathMap_unfollowPacket": "Déverrouiller la vue du paquet",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Régions",
"settings_regionSettingsSubtitle": "Gérer les régions stockées",
"settings_regionManagement_screenTitle": "Gestion de région",
"settings_regionNameHint": "Entrer le nom de la région",
"settings_regionAddRegion": "Ajouter une région",
"settings_regionFetchRegions": "Récupérer les régions à partir des répéteurs",
"settings_regionFetchRegionsFail": "Aucune région n'a été trouvée",
"settings_regionFetchRegionsAlreadyExists": "Cette région a déjà été ajoutée",
"settings_regionName": "Nom de la région",
"settings_regionDeleted": "Région supprimée",
"settings_deleteRegion": "Supprimer la région",
"settings_deleteRegionConfirm": "Supprimez {region} de la liste des régions ?",
"settings_infoHardware": "Matériel",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 octet",
"repeater_pathHashModeOption1": "1 à 2 octets",
"repeater_pathHashModeOption2": "2 à 3 octets",
"repeater_pathHashModeOption3": "3 à 4 octets",
"appSettings_batteryLipoHv": "Tension élevée LiPo (3,0-4,35 V)",
"channels_regionSetTo": "Région : {region}",
"channels_regionNotSet": "Région : aucune",
"channels_regionSelect_Title": "Choisissez une région",
"channels_clearRegion": "Zone dégagée",
"chat_sendImage": "Envoyer l'image",
"chat_imagePickFailed": "Je n'ai pas pu ouvrir cette image",
"chat_receivedGif": "Reçu un GIF",
"repeater_keySettings": "Modifier les clés d'identité",
"repeater_keySettingsSubtitle": "Modifier la paire de clés publique/privée",
"repeater_prvKey": "Clé privée",
"repeater_prvKeyHelper": "Une nouvelle clé privée pour le répéteur, une chaîne hexadécimale de 128 caractères.",
"repeater_generatePrvKey": "Générez une paire de clés aléatoire",
"repeater_stopGeneratingPrvKey": "Arrêter la recherche de la paire de clés",
"repeater_pubKey": "Clé publique",
"repeater_pubKeyHelper": "Voici la clé publique associée à la clé privée générée. Vous ne pouvez pas la définir directement.",
"repeater_pubKeyPrefix": "Préfixe souhaité",
"repeater_pubKeyPrefixHelper": "Trouvez une clé publique qui commence par ces chiffres hexadécimaux. Nombre de tentatives nécessaires : {tries}.",
"imageMessages_enableTitle": "Messages avec images",
"imageMessages_enableSubtitle": "Envoyer des images via le maillage. Nécessite un téléchargement unique du modèle d'image.",
"imageMessages_modelSectionTitle": "Modèle d'image",
"imageMessages_downloadModel": "Télécharger",
"imageMessages_cancelDownload": "Annuler",
"imageMessages_removeModel": "Supprimer le modèle",
"imageMessages_modelReady": "Prêt",
"imageMessages_modelNotPublished": "Non publié — cette version ne peut pas le télécharger.",
"imageMessages_downloadFailed": "Le modèle d'image ne pouvait pas être téléchargé.",
"imageMessages_autoProcessTitle": "Traiter automatiquement les images",
"imageMessages_autoProcessSubtitle": "Reconstruisez chaque image dès son arrivée. Cela utilise environ 2 Go de mémoire pendant une seconde à chaque fois ; désactivez la reconstruction au toucher pour éviter cela.",
"imageSend_title": "Envoyer l'image",
"imageSend_cropNote": "Redimensionné à 512 × 512 · le rapport daspect na pas été conservé",
"imageSend_originalSize": "Texte original\n\nVeuillez traduire le texte suivant en français :",
"imageSend_onAirSize": "En direct",
"imageSend_quality": "Qualité",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Haut",
"imageSend_packetsLabel": "Paquets",
"imageSend_airtimeLabel": "Durée de diffusion",
"imageSend_sizeLabel": "Charge utile",
"imageSend_packetsCount": "{count} {count, plural, =1{paquet} other{paquets}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Réglages de la radio inconnus",
"imageSend_radioUnknownBody": "Connectez-vous à un appareil afin que l'heure de diffusion puisse être calculée.",
"imageSend_longSendTitle": "Longue transmission",
"imageSend_longSendBody": "Cela maintiendra la chaîne pendant environ {duration}.",
"imageSend_floodNote": "Routage des inondations : chaque répéteur dans la zone retransmet chaque paquet, de sorte que le canal reste occupé plus longtemps que cela.",
"imageSend_parityTitle": "Paquet de récupération",
"imageSend_paritySubtitle": "Un paquet supplémentaire. Les messages de groupe ne sont pas reconnus, ce qui permet au destinataire de reconstruire l'image si un seul paquet est perdu.",
"imageSend_send": "Envoyer",
"imageSend_cancel": "Annuler",
"imageSend_encodeFailed": "Cette image ne peut pas être encodée.",
"imageSend_codecDownloading": "Le modèle d'image est toujours en téléchargement.",
"imageSend_codecUnavailable": "L'envoi d'images n'est pas disponible sur cet appareil.",
"imageSend_codecDisabled": "Les messages d'images sont désactivés dans les paramètres.",
"imageSend_deviceUnsupported": "Cette radio ne peut pas envoyer de paquets d'images. Connectez un appareil exécutant le firmware compagnon version 13 ou ultérieure.",
"imageSend_directMessagesUnsupported": "Les images se déplacent en tant que données de groupe, elles ne peuvent donc être envoyées que sur un canal — et non par message direct.",
"imageSend_tooLarge": "Cette image a été encodée en plus de paquets que le format de maillage ne permet.",
"imageSend_sentConfirmation": "Image envoyée en {count} {count, plural, =1{paquet} other{paquets}}.",
"imageSend_sendFailed": "L'image n'a pas pu être envoyée : {error}",
"imageSend_sendingProgress": "Envoi de l'image — paquet {sent} sur {total}",
"receivedImage_senderPrefix": "Nœud {prefix}",
"receivedImage_incoming": "{received} sur {total} paquets",
"receivedImage_queued": "En attendant de décoder",
"receivedImage_tapToDecode": "Tapez pour décoder",
"receivedImage_decoding": "Reconstitution… denviron 1 s",
"receivedImage_incomplete": "Image incomplète — {received} sur {total} paquets reçus",
"receivedImage_corrupt": "L'image ne pouvait pas être reconstruite",
"receivedImage_decoderMissing": "Image reçue — le décodage de l'image est interrompu",
"receivedImage_evicted": "Image non stockée",
"receivedImage_retry": "Réessayez",
"receivedImage_decodeAgain": "Décoder à nouveau",
"receivedImage_openSettings": "Mettre en place",
"receivedImage_tapToProcess": "Tapez pour traiter",
"receivedImage_awaiting": "{bytes} octets · {packets} {packets, plural, =1{paquet} other{paquets}}",
"imageSend_secondsValue": "{seconds} s",
"imageSend_minutesSecondsValue": "{minutes} minutes {seconds} secondes"
} }
+288 -1
View File
@@ -161,6 +161,8 @@
"settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.", "settings_locationIntervalInvalid": "Az intervallumnak legalább 60 másodpercnek kell lennie, és kevesebbnek kell lennie 86 400 másodpercnél.",
"settings_latitude": "Szélesség", "settings_latitude": "Szélesség",
"settings_longitude": "Hosszúság", "settings_longitude": "Hosszúság",
"settings_autoZeroHopAdvertOnGpsUpdate": "Automatikus zero-hop hirdetés GPS-frissítéskor",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).",
"settings_contactSettings": "Kapcsolati beállítások", "settings_contactSettings": "Kapcsolati beállítások",
"settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.", "settings_contactSettingsSubtitle": "A névjegyek hozzáadásának beállításai.",
"settings_privacyMode": "Adatvédelmi mód", "settings_privacyMode": "Adatvédelmi mód",
@@ -332,6 +334,19 @@
"appSettings_last6Hours": "Utolsó 6 óra", "appSettings_last6Hours": "Utolsó 6 óra",
"appSettings_last24Hours": "Az elmúlt 24 óra", "appSettings_last24Hours": "Az elmúlt 24 óra",
"appSettings_lastWeek": "Múlt héten", "appSettings_lastWeek": "Múlt héten",
"appSettings_rasterTileSource": "Raszteres csempeforrás",
"appSettings_stadiaEndpoint": "Stadia végpont",
"appSettings_stadiaApiKey": "Stadia API-kulcs",
"appSettings_stadiaApiKeyRequired": "A Stadia Maps használatához kötelező",
"appSettings_stadiaApiKeyConfigured": "Konfigurálva: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.",
"appSettings_offlineMapCache": "Offline térképgyorsítótár", "appSettings_offlineMapCache": "Offline térképgyorsítótár",
"appSettings_unitsTitle": "Egységek", "appSettings_unitsTitle": "Egységek",
"appSettings_unitsMetric": "Metrikus (m/km)", "appSettings_unitsMetric": "Metrikus (m/km)",
@@ -952,6 +967,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}", "mapCache_boundsLabel": "É {north}, D {south}, K {east}, Ny {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2578,5 +2643,227 @@
"pathMap_noLocation": "Nincs hely", "pathMap_noLocation": "Nincs hely",
"pathMap_followPacket": "Nézet zárolása a csomaghoz", "pathMap_followPacket": "Nézet zárolása a csomaghoz",
"pathMap_unfollowPacket": "A nézet feloldása a csomagból", "pathMap_unfollowPacket": "A nézet feloldása a csomagból",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Tájak",
"settings_regionSettingsSubtitle": "Kincstani megjelenítette gyakorlatos területeket",
"settings_regionManagement_screenTitle": "Regionális Kezelés",
"settings_regionNameHint": "Felhasználja a regioni név",
"settings_regionAddRegion": "Egyes region",
"settings_regionFetchRegions": "Féregelmezők a repeaterekból",
"settings_regionFetchRegionsFail": "Nem található egyéb területek",
"settings_regionFetchRegionsAlreadyExists": "Ez a terítet nemisértették meg.",
"settings_regionName": "Feldúr Neve",
"settings_regionDeleted": "Felült region",
"settings_deleteRegion": "Felajánlatos Tér",
"settings_deleteRegionConfirm": "Kell kiszámítsd a {region}ot a részekre?",
"settings_infoHardware": "Hardverő",
"settings_infoFirmware": "Firmver",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 szavak",
"repeater_pathHashModeOption2": "2 - 3 bajt",
"repeater_pathHashModeOption3": "3 - 4 bajt",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"channels_regionSetTo": "Tájékoztatás: A {region} -t\n\nEz a tájékoztatás a magyar nyelvben a következően kell szóva:\n\nTájékoztatás: A {region} -t",
"channels_regionNotSet": "Tájékoztetés: nem",
"channels_regionSelect_Title": "Válassz egy regionot",
"channels_clearRegion": "Kéres kör",
"chat_sendImage": "Felhasználj az image",
"chat_imagePickFailed": "Nem tudtam ezt a fenti fényre kapni",
"chat_receivedGif": "GIF-ot kapott",
"repeater_keySettings": "Képzőítsekei jelölt",
"repeater_keySettingsSubtitle": "Közelítse a közlekedési/private képlékt",
"repeater_prvKey": "Kis házas",
"repeater_prvKeyHelper": "Kínai nyelvben:\n\nA repeteróhoz új képes kód, 128 karakteri hex-kód.",
"repeater_generatePrvKey": "Egy random képléktár generáld meg",
"repeater_stopGeneratingPrvKey": "Kéypair szavakat kérdéset kaphatás",
"repeater_pubKey": "Kiváló kiegészítő",
"repeater_pubKeyHelper": "Ez a kiváló kód, amely tükrözi a gyártott kiegészítő kódot. Ezet nem lehet megjelenítani direktusan.",
"repeater_pubKeyPrefix": "Kiváló prefix",
"repeater_pubKeyPrefixHelper": "Kiválasztsa meg a közös kódot, amely az alábbi hex-kifüggőnek előre van. Várható kiválasztások száma: {tries}.",
"imageMessages_enableTitle": "Fenti hírüket biztosítani",
"imageMessages_enableSubtitle": "Fényokat a hálózatban küldd el. Egyértelmű fénymodell hozzáadásra kell.",
"imageMessages_modelSectionTitle": "Image model",
"imageMessages_downloadModel": "Hozzáladás",
"imageMessages_cancelDownload": "Színviselés",
"imageMessages_removeModel": "Kisnövezdés",
"imageMessages_modelReady": "Hozzá",
"imageMessages_modelNotPublished": "Érkeztetve nem — ez a építési módszert nem lehet lejobbítani.",
"imageMessages_downloadFailed": "Életmódelmát nem tudtam hozni.",
"imageMessages_autoProcessTitle": "Automatikusan kínaltatja fénybetek",
"imageMessages_autoProcessSubtitle": "Minden fenti image-t kiválasztsa meg, amikor érkezik. Minden más esetben 2 GB fényre kell szükséges; megoldja a rekonstruálisi a támogatásra egy klikkel.",
"imageSend_title": "Felhasználj az image",
"imageSend_cropNote": "512 × 512 órák számára megosztva · képszöget nem biztosítva",
"imageSend_originalSize": "Következtetés:",
"imageSend_onAirSize": "Erőben",
"imageSend_quality": "Hosszú Képesség",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "High: Magas",
"imageSend_packetsLabel": "Paketek",
"imageSend_airtimeLabel": "Kiválasztás a fent",
"imageSend_sizeLabel": "Kártya",
"imageSend_packetsCount": "{count} {count, plural, =1{csomag} other{csomag}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radiooldalok nem tudom",
"imageSend_radioUnknownBody": "Kizárdhoz kapcsolódd a rendszert, hogy a súly az az álló időt lehet elismerni.",
"imageSend_longSendTitle": "Uzun hírás",
"imageSend_longSendBody": "Ez megbízható a kanal {duration}-n.",
"imageSend_floodNote": "Fúró irányozás: Minden repeater a fúró körüléggel megköveti mindkét paketet, ezért a kanal eltartja más a lehetséges időt.",
"imageSend_parityTitle": "Korzolás paksa",
"imageSend_paritySubtitle": "Egy kibőbbes kapszula. Gruppos hírselek nem érkezik, ezért az empfangó nem kell elismerni a jel, ha egy egyes kapszula kaland, hogy nem teljesen érkezz.",
"imageSend_send": "Keresés",
"imageSend_cancel": "Színviselés",
"imageSend_encodeFailed": "Ez a fényet nem tudtam kódolni.",
"imageSend_codecDownloading": "Érdekesz az image model megstálása még van.",
"imageSend_codecUnavailable": "Felhasználó észintje nem kapható ezen a rendszert.",
"imageSend_codecDisabled": "Image hírletek belsőoldal elkalmáznak a konfigurációban.",
"imageSend_deviceUnsupported": "Ez a radio nem tudja verseni színekfájltakat. Habilítd a kompatibilis firmware 13 vagy újabb versionot kapcsált eszközet.",
"imageSend_directMessagesUnsupported": "Fényiak gyakorolják grupos adatban, ezért nem lehetek versenek egy hozzáfűzésre, csak egy kanalra.",
"imageSend_tooLarge": "A figyelmetes fentet nem kínálhat a körközlettel ismétálás elterjesztésére.",
"imageSend_sentConfirmation": "Kép elküldve {count} {count, plural, =1{csomagként} other{csomagként}}.",
"imageSend_sendFailed": "Fentami nem tűnik: {error}",
"imageSend_sendingProgress": "Felhasználó: Lehet a következőkét kifejezetet {sent} paket {total}",
"receivedImage_senderPrefix": "Node {prefix}",
"receivedImage_incoming": "Kulcsfontosságú: Kiválasztja meg a {received} {total} paketot",
"receivedImage_queued": "Kódoláshoz küldőzve",
"receivedImage_tapToDecode": "Kódoláshoz klikd el",
"receivedImage_decoding": "Korlátozás… egy 1 perc után",
"receivedImage_incomplete": "Kérem, az {received} {total} paketet kapottak.",
"receivedImage_corrupt": "Fény nem tudtam megrealítani",
"receivedImage_decoderMissing": "Kezd az image — image decodálás nem kapható",
"receivedImage_evicted": "Fény nem tartalmazva",
"receivedImage_retry": "Kérem kell próbálni",
"receivedImage_decodeAgain": "Kodoldó",
"receivedImage_openSettings": "Jelzése",
"receivedImage_tapToProcess": "Klikd a feljelenítést",
"receivedImage_awaiting": "{bytes} bájt · {packets} {packets, plural, =1{csomag} other{csomag}}",
"imageSend_secondsValue": "{seconds} sekünd\n\n(Note: The placeholder {seconds} is kept unchanged as requested.)",
"imageSend_minutesSecondsValue": "{minutes} per {seconds} sekünd\n\n(Note: The original text appears to be a placeholder format rather than a complete sentence. The translation reflects the literal structure while preserving the placeholders as requested.)"
} }
+289 -49
View File
@@ -240,6 +240,19 @@
"appSettings_last6Hours": "Ultimi 6 ore", "appSettings_last6Hours": "Ultimi 6 ore",
"appSettings_last24Hours": "Ultime 24 ore", "appSettings_last24Hours": "Ultime 24 ore",
"appSettings_lastWeek": "La settimana scorsa", "appSettings_lastWeek": "La settimana scorsa",
"appSettings_rasterTileSource": "Sorgente tile raster",
"appSettings_stadiaEndpoint": "Endpoint Stadia",
"appSettings_stadiaApiKey": "Chiave API Stadia",
"appSettings_stadiaApiKeyRequired": "Obbligatoria per usare Stadia Maps",
"appSettings_stadiaApiKeyConfigured": "Configurata: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.",
"appSettings_offlineMapCache": "Cache Mappa Offline", "appSettings_offlineMapCache": "Cache Mappa Offline",
"appSettings_noAreaSelected": "Nessun'area selezionata", "appSettings_noAreaSelected": "Nessun'area selezionata",
"appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Area selezionata (zoom {minZoom}-{maxZoom})",
@@ -837,6 +850,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1175,7 +1238,7 @@
"repeater_advancedSettings": "Avanzato", "repeater_advancedSettings": "Avanzato",
"repeater_advancedSettingsSubtitle": "Manopole di regolazione per operatori esperti", "repeater_advancedSettingsSubtitle": "Manopole di regolazione per operatori esperti",
"repeater_pathHashMode": "Modalità di hashing del percorso", "repeater_pathHashMode": "Modalità di hashing del percorso",
"repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).", "repeater_pathHashModeHelper": "Byte utilizzati per codificare l'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.",
"repeater_txDelay": "Ritardo a Flood, TX", "repeater_txDelay": "Ritardo a Flood, TX",
"repeater_txDelayHelper": "Riassegnare lo spazio tra i pacchetti per gestire il traffico intenso, come un moltiplicatore del tempo di trasmissione (da 0 a 2, valore predefinito 0,5). Un valore più alto significa meno collisioni, ma una trasmissione più lenta.", "repeater_txDelayHelper": "Riassegnare lo spazio tra i pacchetti per gestire il traffico intenso, come un moltiplicatore del tempo di trasmissione (da 0 a 2, valore predefinito 0,5). Un valore più alto significa meno collisioni, ma una trasmissione più lenta.",
"repeater_directTxDelay": "Ritardo diretto TX", "repeater_directTxDelay": "Ritardo diretto TX",
@@ -1673,7 +1736,6 @@
"settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.", "settings_locationGPSEnableSubtitle": "Abilita l'aggiornamento automatico della posizione tramite GPS.",
"settings_locationIntervalSec": "Intervallo GPS (Secondi)", "settings_locationIntervalSec": "Intervallo GPS (Secondi)",
"settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.", "settings_locationIntervalInvalid": "L'intervallo deve essere di almeno 60 secondi e inferiore a 86400 secondi.",
"contacts_manageRoom": "Gestisci server stanza",
"room_management": "Gestione del Server di Camera", "room_management": "Gestione del Server di Camera",
"@community_joinConfirmation": { "@community_joinConfirmation": {
"placeholders": { "placeholders": {
@@ -1883,7 +1945,6 @@
} }
}, },
"notification_receivedNewMessage": "Nuovo messaggio ricevuto", "notification_receivedNewMessage": "Nuovo messaggio ricevuto",
"notification_receivedNewMessage": "Nuovo messaggio ricevuto",
"settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX", "settings_gpxExportRepeaters": "Esporta ripetitori / server di stanza in GPX",
"settings_gpxExportContacts": "Esporta compagni in GPX", "settings_gpxExportContacts": "Esporta compagni in GPX",
"settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.", "settings_gpxExportSuccess": "Esportazione del file GPX completata con successo.",
@@ -2058,52 +2119,7 @@
"listFilter_addToFavorites": "Aggiungi ai preferiti", "listFilter_addToFavorites": "Aggiungi ai preferiti",
"listFilter_removeFromFavorites": "Rimuovi dai preferiti", "listFilter_removeFromFavorites": "Rimuovi dai preferiti",
"listFilter_favorites": "Preferiti", "listFilter_favorites": "Preferiti",
"@contacts_searchFavorites": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchUsers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRepeaters": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"@contacts_searchRoomServers": {
"placeholders": {
"number": {
"type": "int"
},
"str": {
"type": "String"
}
}
},
"contacts_searchUsers": "Cerca {number}{str} utenti...",
"contacts_searchContactsNoNumber": "Cerca contatti...",
"contacts_searchFavorites": "Cerca {number}{str} preferiti...",
"contacts_unread": "Non letti", "contacts_unread": "Non letti",
"contacts_searchRepeaters": "Cerca {number}{str} ripetitori...",
"contacts_searchRoomServers": "Cerca {number}{str} server stanza...",
"contactsSettings_title": "Impostazioni dei contatti", "contactsSettings_title": "Impostazioni dei contatti",
"settings_contactSettings": "Impostazioni di contatto", "settings_contactSettings": "Impostazioni di contatto",
"contactsSettings_otherTitle": "Altre impostazioni relative ai contatti", "contactsSettings_otherTitle": "Altre impostazioni relative ai contatti",
@@ -2207,6 +2223,8 @@
"settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria", "settings_telemetryEnvironmentMode": "Modalità di ambiente di telemetria",
"settings_advertLocation": "Posizione dell'annuncio", "settings_advertLocation": "Posizione dell'annuncio",
"settings_advertLocationSubtitle": "Includi la posizione nell'annuncio", "settings_advertLocationSubtitle": "Includi la posizione nell'annuncio",
"settings_autoZeroHopAdvertOnGpsUpdate": "Annuncio zero-hop automatico all'aggiornamento GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell'annuncio).",
"settings_privacy": "Impostazioni sulla privacy", "settings_privacy": "Impostazioni sulla privacy",
"settings_denyAll": "Negare tutto", "settings_denyAll": "Negare tutto",
"settings_privacySubtitle": "Controlla le informazioni che vengono condivise.", "settings_privacySubtitle": "Controlla le informazioni che vengono condivise.",
@@ -2721,5 +2739,227 @@
"pathMap_noLocation": "Nessuna posizione", "pathMap_noLocation": "Nessuna posizione",
"pathMap_followPacket": "Blocca la vista sul pacchetto", "pathMap_followPacket": "Blocca la vista sul pacchetto",
"pathMap_unfollowPacket": "Sblocca la vista dal pacchetto", "pathMap_unfollowPacket": "Sblocca la vista dal pacchetto",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regioni",
"settings_regionSettingsSubtitle": "Gestione delle regioni salvate",
"settings_regionManagement_screenTitle": "Gestione Regionale",
"settings_regionNameHint": "Inserisci il nome della regione",
"settings_regionAddRegion": "Aggiungi regione",
"settings_regionFetchRegions": "Recupera le regioni dai ripetitori",
"settings_regionFetchRegionsFail": "Non sono state trovate regioni",
"settings_regionFetchRegionsAlreadyExists": "Questa regione è già stata aggiunta",
"settings_regionName": "Nome della Regione",
"settings_regionDeleted": "Regione cancellata",
"settings_deleteRegion": "Elimina Regione",
"settings_deleteRegionConfirm": "Rimuovere \"{region}\" dalla lista delle regioni?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 byte",
"repeater_pathHashModeOption2": "2 - 3 byte",
"repeater_pathHashModeOption3": "3 - 4 byte",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Regione: {region}",
"channels_regionNotSet": "Regione: nessuna",
"channels_regionSelect_Title": "Scegli una regione",
"channels_clearRegion": "Zona chiara",
"chat_sendImage": "Invia immagine",
"chat_imagePickFailed": "Non sono riuscito ad aprire quell'immagine",
"chat_receivedGif": "Ricevuto un GIF",
"repeater_keySettings": "Cambia le Chiavi di Identità",
"repeater_keySettingsSubtitle": "Cambia la coppia di chiavi pubblica/privata",
"repeater_prvKey": "Chiave privata",
"repeater_prvKeyHelper": "Una nuova chiave privata per il ripetitore, una stringa esadecimale di 128 caratteri.",
"repeater_generatePrvKey": "Genera una coppia di chiavi casuale",
"repeater_stopGeneratingPrvKey": "Interrompi la ricerca della coppia di chiavi",
"repeater_pubKey": "Chiave pubblica",
"repeater_pubKeyHelper": "Questa è la chiave pubblica che accompagna la chiave privata generata. Non è possibile impostarla direttamente.",
"repeater_pubKeyPrefix": "Prefisso desiderato",
"repeater_pubKeyPrefixHelper": "Trova una chiave pubblica che inizia con queste cifre esadecimali. Tentativi necessari previsti: {tries}.",
"imageMessages_enableTitle": "Messaggi con immagini",
"imageMessages_enableSubtitle": "Invia le immagini tramite la mesh. È necessario scaricare il modello dell'immagine una tantum.",
"imageMessages_modelSectionTitle": "Modello di immagine",
"imageMessages_downloadModel": "Scarica",
"imageMessages_cancelDownload": "Annullare",
"imageMessages_removeModel": "Rimuovi il modello",
"imageMessages_modelReady": "Pronto",
"imageMessages_modelNotPublished": "Non pubblicato ancora — questa versione non può scaricarlo.",
"imageMessages_downloadFailed": "Il modello di immagine non può essere scaricato.",
"imageMessages_autoProcessTitle": "Elabora automaticamente le immagini",
"imageMessages_autoProcessSubtitle": "Ricostruisci ogni immagine non appena arriva. Utilizza circa 2 GB di memoria per un secondo ogni volta; disattiva la ricostruzione con un semplice tocco.",
"imageSend_title": "Invia immagine",
"imageSend_cropNote": "Ridimensionato a 512 × 512 · rapporto d'aspetto non conservato",
"imageSend_originalSize": "Traduzione:\n\nOriginal text:\n\nPlease translate the following English text into Italian:\n\nOriginal text:\n\n(No original English text provided)",
"imageSend_onAirSize": "In onda",
"imageSend_quality": "Qualità",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Alto",
"imageSend_packetsLabel": "Pacchetti",
"imageSend_airtimeLabel": "Tempo in onda",
"imageSend_sizeLabel": "Carico utile",
"imageSend_packetsCount": "{count} {count, plural, =1{pacchetto} other{pacchetti}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Impostazioni radio sconosciute",
"imageSend_radioUnknownBody": "Collegati a un dispositivo in modo che si possa calcolare l'orario di trasmissione.",
"imageSend_longSendTitle": "Lunga trasmissione",
"imageSend_longSendBody": "Questo manterrà il canale per circa {duration}.",
"imageSend_floodNote": "Instradamento delle inondazioni: ogni ripetitore nel raggio ritrasmette ogni pacchetto, quindi il canale rimane occupato più a lungo.",
"imageSend_parityTitle": "Pacchetto di recupero",
"imageSend_paritySubtitle": "Un pacchetto aggiuntivo. I messaggi di gruppo non vengono riconosciuti, quindi questo permette al destinatario di ricostruire l'immagine se un singolo pacchetto viene perso.",
"imageSend_send": "Invia",
"imageSend_cancel": "Annullare",
"imageSend_encodeFailed": "Questa immagine non può essere codificata.",
"imageSend_codecDownloading": "Il modello di immagine sta ancora scaricando.",
"imageSend_codecUnavailable": "L'invio di immagini non è disponibile su questo dispositivo.",
"imageSend_codecDisabled": "I messaggi immagine sono disattivati nelle impostazioni.",
"imageSend_deviceUnsupported": "Questa radio non può inviare pacchetti di immagini. Collegare un dispositivo con firmware companion versione 13 o successiva.",
"imageSend_directMessagesUnsupported": "Le immagini viaggiano come dati di gruppo, quindi possono essere inviate solo a un canale — non tramite messaggio diretto.",
"imageSend_tooLarge": "Quella immagine è stata codificata in un numero di pacchetti superiore a quello consentito dal formato mesh.",
"imageSend_sentConfirmation": "Immagine inviata come {count} {count, plural, =1{pacchetto} other{pacchetti}}.",
"imageSend_sendFailed": "L'immagine non può essere inviata: {error}",
"imageSend_sendingProgress": "Invio immagine — pacchetto {sent} di {total}",
"receivedImage_senderPrefix": "Nodo {prefix}",
"receivedImage_incoming": "{received} di {total} pacchetti",
"receivedImage_queued": "In attesa di decodificare",
"receivedImage_tapToDecode": "Tocca per decodificare",
"receivedImage_decoding": "Ricostruire… circa 1 s",
"receivedImage_incomplete": "Immagine incompleta — {received} su {total} pacchetti ricevuti",
"receivedImage_corrupt": "L'immagine non potrebbe essere ricostruita",
"receivedImage_decoderMissing": "Immagine ricevuta — decodifica dell'immagine non funzionante",
"receivedImage_evicted": "Immagine non più memorizzata",
"receivedImage_retry": "Prova ancora",
"receivedImage_decodeAgain": "Decodifica di nuovo",
"receivedImage_openSettings": "Imposta",
"receivedImage_tapToProcess": "Premi per elaborare",
"receivedImage_awaiting": "{bytes} byte · {packets} {packets, plural, =1{pacchetto} other{pacchetti}}",
"imageSend_secondsValue": "{seconds} secondi",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+293 -12
View File
@@ -28,11 +28,11 @@
"common_enable": "有効にする", "common_enable": "有効にする",
"common_disable": "無効にする", "common_disable": "無効にする",
"common_undo": "元に戻す", "common_undo": "元に戻す",
"messageStatus_sent": "送信済み", "messageStatus_sent": "送信",
"messageStatus_delivered": "配信済み", "messageStatus_delivered": "配",
"messageStatus_pending": "送信", "messageStatus_pending": "送信",
"messageStatus_failed": "送信に失敗", "messageStatus_failed": "送信できませんでした",
"messageStatus_repeated": "重複受信", "messageStatus_repeated": "何度も聞いた",
"common_autoRefresh": "自動更新", "common_autoRefresh": "自動更新",
"common_interval": "間隔", "common_interval": "間隔",
"common_reboot": "再起動", "common_reboot": "再起動",
@@ -167,6 +167,8 @@
"settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。", "settings_locationIntervalInvalid": "間隔は少なくとも60秒で、86400秒未満でなければなりません。",
"settings_latitude": "緯度", "settings_latitude": "緯度",
"settings_longitude": "経度", "settings_longitude": "経度",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS更新時にゼロホップ広告を自動送信",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。",
"settings_contactSettings": "連絡先設定", "settings_contactSettings": "連絡先設定",
"settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定", "settings_contactSettingsSubtitle": "連絡先の追加方法に関する設定",
"settings_privacyMode": "プライバシーモード", "settings_privacyMode": "プライバシーモード",
@@ -338,6 +340,19 @@
"appSettings_last6Hours": "過去6時間", "appSettings_last6Hours": "過去6時間",
"appSettings_last24Hours": "過去24時間", "appSettings_last24Hours": "過去24時間",
"appSettings_lastWeek": "過去1週間", "appSettings_lastWeek": "過去1週間",
"appSettings_rasterTileSource": "ラスタタイルのソース",
"appSettings_stadiaEndpoint": "Stadia エンドポイント",
"appSettings_stadiaApiKey": "Stadia API キー",
"appSettings_stadiaApiKeyRequired": "Stadia Maps の利用に必要です",
"appSettings_stadiaApiKeyConfigured": "設定済み: {maskedKey}",
"@appSettings_stadiaApiKeyConfigured": {
"placeholders": {
"maskedKey": {
"type": "String"
}
}
},
"appSettings_stadiaApiKeyDialogDescription": "Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。",
"appSettings_offlineMapCache": "オフライン地図キャッシュ", "appSettings_offlineMapCache": "オフライン地図キャッシュ",
"appSettings_unitsTitle": "単位", "appSettings_unitsTitle": "単位",
"appSettings_unitsMetric": "メートル法 (m / km)", "appSettings_unitsMetric": "メートル法 (m / km)",
@@ -970,6 +985,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -2532,13 +2597,7 @@
} }
} }
}, },
"messageStatus_delivered": "配達",
"common_undo": "元に戻す",
"messageStatus_pending": "送信",
"messageStatus_sent": "送信",
"messageStatus_failed": "送信できませんでした",
"contacts_moreOptions": "さらに多くの選択肢", "contacts_moreOptions": "さらに多くの選択肢",
"messageStatus_repeated": "何度も聞いた",
"contacts_searchOpen": "連絡先を検索する", "contacts_searchOpen": "連絡先を検索する",
"contacts_searchClose": "検索を終了", "contacts_searchClose": "検索を終了",
"routing_modeFlood": "フラッド", "routing_modeFlood": "フラッド",
@@ -2680,5 +2739,227 @@
"pathMap_noLocation": "位置情報なし", "pathMap_noLocation": "位置情報なし",
"pathMap_followPacket": "パケットを追跡", "pathMap_followPacket": "パケットを追跡",
"pathMap_unfollowPacket": "パケットの追跡を解除", "pathMap_unfollowPacket": "パケットの追跡を解除",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "地域",
"settings_regionSettingsSubtitle": "保存された領域の管理",
"settings_regionManagement_screenTitle": "地域管理",
"settings_regionNameHint": "地域名を入力してください",
"settings_regionAddRegion": "地域を追加",
"settings_regionFetchRegions": "リピーターからのフェッチ地域",
"settings_regionFetchRegionsFail": "地域は見つかりませんでした",
"settings_regionFetchRegionsAlreadyExists": "この地域はすでに追加されています。",
"settings_regionName": "地域名",
"settings_regionDeleted": "地域が削除されました",
"settings_deleteRegion": "地域を削除",
"settings_deleteRegionConfirm": "{region} を地域リストから削除しますか?",
"settings_infoHardware": "ハードウェア",
"settings_infoFirmware": "ファームウェア",
"repeater_pathHashModeOption0": "0 - 1バイト",
"repeater_pathHashModeOption1": "1〜2バイト",
"repeater_pathHashModeOption2": "2〜3バイト",
"repeater_pathHashModeOption3": "3〜4バイト",
"appSettings_batteryLipoHv": "リポバッテリー HV3.0〜4.35V",
"channels_regionSetTo": "地域:{region}",
"channels_regionNotSet": "地域:なし",
"channels_regionSelect_Title": "地域を選択する",
"channels_clearRegion": "クリアな地域",
"chat_sendImage": "画像を送信",
"chat_imagePickFailed": "その画像を開けませんでした",
"chat_receivedGif": "GIFを受け取りました",
"repeater_keySettings": "アイデンティティキーの変更",
"repeater_keySettingsSubtitle": "公開鍵/秘密鍵を変更する",
"repeater_prvKey": "プライベートキー",
"repeater_prvKeyHelper": "リピーター用の新しいプライベートキー。128文字の16進文字列です。",
"repeater_generatePrvKey": "ランダムな鍵ペアを生成する",
"repeater_stopGeneratingPrvKey": "キーペアの検索を中断",
"repeater_pubKey": "公開鍵",
"repeater_pubKeyHelper": "これは生成された秘密鍵に対応する公開鍵です。直接設定することはできません。",
"repeater_pubKeyPrefix": "希望する接頭辞",
"repeater_pubKeyPrefixHelper": "これらの16進数で始まる公開鍵を見つけてください。予想される試行回数: {tries}。",
"imageMessages_enableTitle": "画像メッセージを有効にする",
"imageMessages_enableSubtitle": "メッシュ経由で画像を送信してください。画像モデルを一度限りのダウンロードが必要です。",
"imageMessages_modelSectionTitle": "画像モデル",
"imageMessages_downloadModel": "ダウンロード",
"imageMessages_cancelDownload": "キャンセル",
"imageMessages_removeModel": "モデルを削除",
"imageMessages_modelReady": "準備完了",
"imageMessages_modelNotPublished": "まだ公開されていません — このビルドではダウンロードできません。",
"imageMessages_downloadFailed": "画像モデルがダウンロードできませんでした。",
"imageMessages_autoProcessTitle": "画像を自動的に処理する",
"imageMessages_autoProcessSubtitle": "画像が届き次第、すぐに再構築してください。1回あたり約2GBのメモリを使用します。タップで再構築を解除できます。",
"imageSend_title": "画像を送信",
"imageSend_cropNote": "512 × 512にリサイズされました。アスペクト比は保持されませんでした。",
"imageSend_originalSize": "原文:\nThe early morning light filtered through the curtains, casting a soft glow on the room.\n\n翻訳:\n早朝の光がカーテンをすり抜け、部屋に柔らかな光を投げかけていた。",
"imageSend_onAirSize": "放送中",
"imageSend_quality": "品質",
"imageSend_qualityStandard": "標準",
"imageSend_qualityHigh": "ハイ",
"imageSend_packetsLabel": "パケット",
"imageSend_airtimeLabel": "放送時間",
"imageSend_sizeLabel": "ペイロード",
"imageSend_packetsCount": "{count} {count, plural, =1{パケット} other{パケット}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "ラジオの設定が不明です",
"imageSend_radioUnknownBody": "放送時間の計算ができるように、デバイスに接続してください。",
"imageSend_longSendTitle": "長距離伝送",
"imageSend_longSendBody": "このチャンネルは約 {duration} 間保持されます。",
"imageSend_floodNote": "洪水ルーティング:範囲内の各リピーターが各パケットを再送信するため、チャネルはこれよりも長く占有されたままになります。",
"imageSend_parityTitle": "回復パケット",
"imageSend_paritySubtitle": "追加のパケットが1つあります。グループメッセージは確認されないため、1つのパケットが失われても受信者が画像を再構築できます。",
"imageSend_send": "送る",
"imageSend_cancel": "キャンセル",
"imageSend_encodeFailed": "この画像はエンコードできませんでした。",
"imageSend_codecDownloading": "画像モデルはまだダウンロード中です。",
"imageSend_codecUnavailable": "このデバイスでは画像送信が利用できません。",
"imageSend_codecDisabled": "設定で画像メッセージはオフになっています。",
"imageSend_deviceUnsupported": "このラジオは画像パケットを送信できません。コンパニオン ファームウェア 13 以降を実行しているデバイスを接続してください。",
"imageSend_directMessagesUnsupported": "画像はグループデータとして送信されるため、ダイレクトメッセージではなくチャンネルに送ることができます。",
"imageSend_tooLarge": "その画像は、メッシュ形式が許容するよりも多くのパケットにエンコードされています。",
"imageSend_sentConfirmation": "{count} {count, plural, =1{パケット} other{パケット}}として画像を送信しました。",
"imageSend_sendFailed": "画像を送信できませんでした:{error}",
"imageSend_sendingProgress": "画像送信 — パケット {sent} 件、合計 {total} 件",
"receivedImage_senderPrefix": "ノード {prefix}",
"receivedImage_incoming": "{total} パケット中 {received} パケット",
"receivedImage_queued": "復号を待っている",
"receivedImage_tapToDecode": "デコードするにはタップしてください。",
"receivedImage_decoding": "再構築… 約1秒",
"receivedImage_incomplete": "画像が不完全です — {total} パケット中 {received} パケットが到着しました",
"receivedImage_corrupt": "画像を再構成することはできませんでした",
"receivedImage_decoderMissing": "画像を受信しました — 画像のデコードが失敗しました",
"receivedImage_evicted": "画像はもう保存されていません",
"receivedImage_retry": "もう一度試してください",
"receivedImage_decodeAgain": "もう一度デコードしてください",
"receivedImage_openSettings": "設定する",
"receivedImage_tapToProcess": "処理するにはタップしてください",
"receivedImage_awaiting": "{bytes} バイト・{packets} {packets, plural, =1{パケット} other{パケット}}",
"imageSend_secondsValue": "{seconds}秒",
"imageSend_minutesSecondsValue": "{minutes}分 {seconds}秒"
} }
+290 -6
View File
@@ -161,6 +161,8 @@
"settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.", "settings_locationIntervalInvalid": "간격은 최소 60초 이상, 86400초 미만이어야 합니다.",
"settings_latitude": "위도", "settings_latitude": "위도",
"settings_longitude": "경도", "settings_longitude": "경도",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 업데이트 시 제로 홉 광고 자동 전송",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).",
"settings_contactSettings": "연락처 설정", "settings_contactSettings": "연락처 설정",
"settings_contactSettingsSubtitle": "연락처 추가 방식 설정", "settings_contactSettingsSubtitle": "연락처 추가 방식 설정",
"settings_privacyMode": "개인 정보 보호 모드", "settings_privacyMode": "개인 정보 보호 모드",
@@ -332,6 +334,19 @@
"appSettings_last6Hours": "지난 6시간", "appSettings_last6Hours": "지난 6시간",
"appSettings_last24Hours": "지난 24시간", "appSettings_last24Hours": "지난 24시간",
"appSettings_lastWeek": "지난 주", "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_offlineMapCache": "오프라인 지도 캐시",
"appSettings_unitsTitle": "단위", "appSettings_unitsTitle": "단위",
"appSettings_unitsMetric": "미터법 (m / km)", "appSettings_unitsMetric": "미터법 (m / km)",
@@ -952,6 +967,56 @@
} }
} }
}, },
"mapCache_cachedTilesLabel": "Cached tiles",
"mapCache_cachedTileSummaryLabel": "Cached tile summary",
"mapCache_bulkDownloadDisabledForSource": "Offline bulk downloads are disabled for {source}.",
"@mapCache_bulkDownloadDisabledForSource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_bulkDownloadDisabledInConfig": "Offline bulk downloads are disabled for {source} in this app configuration.",
"@mapCache_bulkDownloadDisabledInConfig": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summarySource": "Source: {source}",
"@mapCache_summarySource": {
"placeholders": {
"source": {
"type": "String"
}
}
},
"mapCache_summaryCachedTilesForSource": "Cached tiles for source: {count}",
"@mapCache_summaryCachedTilesForSource": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryCachedInSelection": "Cached in selected area/zoom: {count}",
"@mapCache_summaryCachedInSelection": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"mapCache_summaryApproxCacheSize": "Approx cache size: {size}",
"@mapCache_summaryApproxCacheSize": {
"placeholders": {
"size": {
"type": "String"
}
}
},
"mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}", "mapCache_boundsLabel": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1293,7 +1358,7 @@
"repeater_advancedSettings": "고급", "repeater_advancedSettings": "고급",
"repeater_advancedSettingsSubtitle": "숙련된 운영자를 위한 조절 노브", "repeater_advancedSettingsSubtitle": "숙련된 운영자를 위한 조절 노브",
"repeater_pathHashMode": "패스 해시 모드", "repeater_pathHashMode": "패스 해시 모드",
"repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개 ID, 최대 64개의 홉), 1=2 바이트 (65,000개 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.", "repeater_pathHashModeHelper": "이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.",
"repeater_txDelay": "플러드 TX 지연", "repeater_txDelay": "플러드 TX 지연",
"repeater_txDelayHelper": "홍수 시 교통량에 맞춰 재전송 간격을 설정합니다. 이는 패킷의 전송 시간을 곱한 값 (0-2, 기본값 0.5)으로 설정합니다. 값이 클수록 충돌이 줄어들지만 전송 속도가 느려집니다.", "repeater_txDelayHelper": "홍수 시 교통량에 맞춰 재전송 간격을 설정합니다. 이는 패킷의 전송 시간을 곱한 값 (0-2, 기본값 0.5)으로 설정합니다. 값이 클수록 충돌이 줄어들지만 전송 속도가 느려집니다.",
"repeater_directTxDelay": "직접적인 TX 지연", "repeater_directTxDelay": "직접적인 TX 지연",
@@ -2430,13 +2495,10 @@
} }
} }
}, },
"messageStatus_pending": "발송",
"messageStatus_sent": "발송",
"messageStatus_delivered": "배송 완료",
"common_undo": "되돌리기",
"messageStatus_pending": "전송 중", "messageStatus_pending": "전송 중",
"messageStatus_sent": "전송됨", "messageStatus_sent": "전송됨",
"messageStatus_delivered": "전달됨", "messageStatus_delivered": "전달됨",
"common_undo": "되돌리기",
"messageStatus_failed": "전송 실패", "messageStatus_failed": "전송 실패",
"messageStatus_repeated": "반복 수신됨", "messageStatus_repeated": "반복 수신됨",
"contacts_searchOpen": "연락처 검색", "contacts_searchOpen": "연락처 검색",
@@ -2677,5 +2739,227 @@
"type": "String" "type": "String"
} }
} }
} },
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "지역",
"settings_regionSettingsSubtitle": "저장된 지역 관리",
"settings_regionManagement_screenTitle": "지역 관리",
"settings_regionNameHint": "지역 이름 입력",
"settings_regionAddRegion": "지역 추가",
"settings_regionFetchRegions": "리피터에서 지역을 가져옵니다.",
"settings_regionFetchRegionsFail": "어떤 지역도 발견되지 않았습니다.",
"settings_regionFetchRegionsAlreadyExists": "이 지역은 이미 추가되었습니다.",
"settings_regionName": "지역 이름",
"settings_regionDeleted": "지역 삭제",
"settings_deleteRegion": "지역 삭제",
"settings_deleteRegionConfirm": "\"{region}\"을(를) 지역 목록에서 삭제하시겠습니까?",
"settings_infoHardware": "하드웨어",
"settings_infoFirmware": "펌웨어",
"repeater_pathHashModeOption0": "0 - 1 바이트",
"repeater_pathHashModeOption1": "1~2 바이트",
"repeater_pathHashModeOption2": "2 - 3 바이트",
"repeater_pathHashModeOption3": "3 - 4 바이트",
"appSettings_batteryLipoHv": "리포 HV (3.0-4.35V)",
"channels_regionSetTo": "지역: {region}",
"channels_regionNotSet": "지역: 없음",
"channels_regionSelect_Title": "지역을 선택하세요",
"channels_clearRegion": "명확한 지역",
"chat_sendImage": "이미지를 보내세요",
"chat_imagePickFailed": "그 이미지를 열 수 없었어요.",
"chat_receivedGif": "GIF를 받았습니다",
"repeater_keySettings": "신원 키 변경",
"repeater_keySettingsSubtitle": "공개/비공개 키 쌍을 변경하세요",
"repeater_prvKey": "개인 키",
"repeater_prvKeyHelper": "리피터의 새로운 개인 키로, 128자리의 16진수 문자열입니다.",
"repeater_generatePrvKey": "임의의 키 쌍을 생성하세요.",
"repeater_stopGeneratingPrvKey": "키페어 검색 중단",
"repeater_pubKey": "공개 키",
"repeater_pubKeyHelper": "이것은 생성된 개인 키와 함께 사용되는 공개 키입니다. 직접 설정할 수 없습니다.",
"repeater_pubKeyPrefix": "원하는 접두사",
"repeater_pubKeyPrefixHelper": "이러한 16진수로 시작하는 공개 키를 찾으세요. 필요한 시도 횟수: {tries}.",
"imageMessages_enableTitle": "이미지 메시지 활성화",
"imageMessages_enableSubtitle": "메시를 통해 이미지를 전송하세요. 일회성 이미지 모델 다운로드가 필요합니다.",
"imageMessages_modelSectionTitle": "이미지 모델",
"imageMessages_downloadModel": "다운로드",
"imageMessages_cancelDownload": "취소",
"imageMessages_removeModel": "모델 제거",
"imageMessages_modelReady": "준비 완료",
"imageMessages_modelNotPublished": "아직 출판되지 않았습니다 — 이 빌드로는 다운로드할 수 없습니다.",
"imageMessages_downloadFailed": "이미지 모델을 다운로드할 수 없었습니다.",
"imageMessages_autoProcessTitle": "자동으로 프로세스 이미지를 처리합니다.",
"imageMessages_autoProcessSubtitle": "이미지가 도착하는 즉시 재구성하세요. 매번 2GB의 메모리를 사용하며, 탭으로 재구성을 원할 경우 해제하세요.",
"imageSend_title": "이미지를 보내세요",
"imageSend_cropNote": "512 × 512로 크기가 조정되었으며, 종횡비는 유지되지 않았습니다.",
"imageSend_originalSize": "원문 없음",
"imageSend_onAirSize": "방송 중",
"imageSend_quality": "품질",
"imageSend_qualityStandard": "표준",
"imageSend_qualityHigh": "높다",
"imageSend_packetsLabel": "패킷",
"imageSend_airtimeLabel": "방송 시간",
"imageSend_sizeLabel": "페이로드",
"imageSend_packetsCount": "{count} {count, plural, =1{패킷} other{패킷}}",
"imageSend_range": "{min}~{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "라디오 설정 정보 없음",
"imageSend_radioUnknownBody": "방송 시간을 계산할 수 있도록 기기와 연결하세요.",
"imageSend_longSendTitle": "장거리 송신",
"imageSend_longSendBody": "이것은 약 {duration} 동안 채널을 유지할 것입니다.",
"imageSend_floodNote": "홍수 라우팅: 범위 내의 모든 리피터가 각 패킷을 재전송하므로, 채널이 이보다 더 오래 점유 상태를 유지합니다.",
"imageSend_parityTitle": "회복 패킷",
"imageSend_paritySubtitle": "추가 한 개의 패킷입니다. 그룹 메시지는 확인되지 않으므로, 하나의 패킷이 손실되었을 경우 수신자가 이미지를 다시 구성할 수 있습니다.",
"imageSend_send": "보내다",
"imageSend_cancel": "취소",
"imageSend_encodeFailed": "이 이미지는 인코딩할 수 없습니다.",
"imageSend_codecDownloading": "이미지 모델이 아직 다운로드 중입니다.",
"imageSend_codecUnavailable": "이 기기에서는 이미지 전송이 불가능합니다.",
"imageSend_codecDisabled": "설정에서 이미지 메시지가 꺼져 있습니다.",
"imageSend_deviceUnsupported": "이 라디오는 이미지 패킷을 전송할 수 없습니다. 컴패니언 펌웨어 13 이상을 실행하는 장치를 연결해 주세요.",
"imageSend_directMessagesUnsupported": "이미지는 그룹 데이터로 전송되므로, 직접 메시지가 아닌 채널로만 보낼 수 있습니다.",
"imageSend_tooLarge": "그 이미지는 메시 형식이 허용하는 것보다 더 많은 패킷으로 인코딩되었습니다.",
"imageSend_sentConfirmation": "이미지가 {count} {count, plural, =1{패킷} other{패킷}}로 전송되었습니다.",
"imageSend_sendFailed": "이미지를 보낼 수 없습니다: {error}",
"imageSend_sendingProgress": "이미지 전송 — {sent}개의 {total} 패킷",
"receivedImage_senderPrefix": "노드 {prefix}",
"receivedImage_incoming": "{total}개 패킷 중 {received}개",
"receivedImage_queued": "해독을 기다리는 중입니다.",
"receivedImage_tapToDecode": "해독하려면 탭을 누르세요.",
"receivedImage_decoding": "재구성하는… 약 1초 정도",
"receivedImage_incomplete": "이미지 불완전 — {total}개 중 {received}개 패킷 수신됨",
"receivedImage_corrupt": "이미지는 재구성될 수 없습니다.",
"receivedImage_decoderMissing": "수신된 이미지 — 이미지 해독이 되지 않았습니다",
"receivedImage_evicted": "이미지는 더 이상 저장되지 않았습니다.",
"receivedImage_retry": "다시 시도해 보세요",
"receivedImage_decodeAgain": "다시 해독하다",
"receivedImage_openSettings": "설정하다",
"receivedImage_tapToProcess": "처리하려면 탭을 누르세요.",
"receivedImage_awaiting": "{bytes} 바이트 · {packets} {packets, plural, =1{패킷} other{패킷}}",
"imageSend_secondsValue": "{seconds} 초",
"imageSend_minutesSecondsValue": "{minutes}분 {seconds}초"
} }
+668 -2
View File
@@ -784,6 +784,78 @@ abstract class AppLocalizations {
/// **'Radio settings updated'** /// **'Radio settings updated'**
String get settings_radioSettingsUpdated; 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. /// No description provided for @settings_location.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -958,6 +1030,18 @@ abstract class AppLocalizations {
/// **'Include location in advert.'** /// **'Include location in advert.'**
String get settings_advertLocationSubtitle; 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. /// No description provided for @settings_multiAck.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1156,6 +1240,18 @@ abstract class AppLocalizations {
/// **'Channel Count'** /// **'Channel Count'**
String get settings_infoChannelCount; 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. /// No description provided for @settings_presets.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1378,6 +1474,30 @@ abstract class AppLocalizations {
/// **'Українська'** /// **'Українська'**
String get appSettings_languageUk; 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. /// No description provided for @appSettings_enableMessageTracing.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1618,6 +1738,12 @@ abstract class AppLocalizations {
/// **'LiPo (3.0-4.2V)'** /// **'LiPo (3.0-4.2V)'**
String get appSettings_batteryLipo; 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. /// No description provided for @appSettings_mapDisplay.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -1720,6 +1846,42 @@ abstract class AppLocalizations {
/// **'Last week'** /// **'Last week'**
String get appSettings_lastWeek; 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. /// No description provided for @appSettings_offlineMapCache.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2458,6 +2620,30 @@ abstract class AppLocalizations {
/// **'e.g. #team'** /// **'e.g. #team'**
String get channels_hashtagHint; 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. /// No description provided for @chat_noMessages.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -2548,6 +2734,24 @@ abstract class AppLocalizations {
/// **'Send GIF'** /// **'Send GIF'**
String get chat_sendGif; 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. /// No description provided for @chat_reply.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -3700,6 +3904,54 @@ abstract class AppLocalizations {
/// **'Failed downloads: {count}'** /// **'Failed downloads: {count}'**
String mapCache_failedDownloads(int 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. /// No description provided for @mapCache_boundsLabel.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -4649,9 +4901,69 @@ abstract class AppLocalizations {
/// No description provided for @repeater_pathHashModeHelper. /// No description provided for @repeater_pathHashModeHelper.
/// ///
/// In en, this message translates to: /// 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; 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. /// No description provided for @repeater_txDelay.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -5423,7 +5735,7 @@ abstract class AppLocalizations {
/// No description provided for @repeater_cliHelpSetPrvKey. /// No description provided for @repeater_cliHelpSetPrvKey.
/// ///
/// In en, this message translates to: /// 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; String get repeater_cliHelpSetPrvKey;
/// No description provided for @repeater_cliHelpSetRadioRxGain. /// No description provided for @repeater_cliHelpSetRadioRxGain.
@@ -7468,6 +7780,72 @@ abstract class AppLocalizations {
/// **'Translation'** /// **'Translation'**
String get translation_title; 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. /// No description provided for @translation_enableTitle.
/// ///
/// In en, this message translates to: /// In en, this message translates to:
@@ -7935,6 +8313,294 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Unlock view from packet'** /// **'Unlock view from packet'**
String get pathMap_unfollowPacket; 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 class _AppLocalizationsDelegate
+424
View File
@@ -367,6 +367,47 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_radioSettingsUpdated => 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 @override
String get settings_location => 'Местоположение'; String get settings_location => 'Местоположение';
@@ -467,6 +508,14 @@ class AppLocalizationsBg extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Включи местоположение в обявата'; 'Включи местоположение в обявата';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Автоматична zero-hop обява при GPS обновяване';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Когато GPS местоположението се промени, изпрати zero-hop обява (изисква местоположение в обявата).';
@override @override
String get settings_multiAck => 'Множество ACK'; String get settings_multiAck => 'Множество ACK';
@@ -579,6 +628,13 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Брой канали'; String get settings_infoChannelCount => 'Брой канали';
@override
String get settings_infoHardware => 'Оборудование';
@override
String get settings_infoFirmware =>
'Програмно-оборонни системни программи (фirmware)';
@override @override
String get settings_presets => 'Предварителни настройки'; String get settings_presets => 'Предварителни настройки';
@@ -694,6 +750,18 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Украински'; 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 @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Разрешаване на проследяване на съобщения'; 'Разрешаване на проследяване на съобщения';
@@ -843,6 +911,9 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)'; String get appSettings_batteryLipo => 'Литиев полимер (3.0-4.2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo ВЗГ (3,0-4,35 В)';
@override @override
String get appSettings_mapDisplay => 'Карта за показване'; String get appSettings_mapDisplay => 'Карта за показване';
@@ -900,6 +971,28 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Миналата седмица'; 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 @override
String get appSettings_offlineMapCache => 'Кеш на офлайн карти'; String get appSettings_offlineMapCache => 'Кеш на офлайн карти';
@@ -1330,6 +1423,20 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'напр. #отбор'; 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 @override
String get chat_noMessages => 'Няма съобщения.'; String get chat_noMessages => 'Няма съобщения.';
@@ -1385,6 +1492,15 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get chat_sendGif => 'Изпрати GIF'; String get chat_sendGif => 'Изпрати GIF';
@override
String get chat_sendImage => 'Изпрати изображение';
@override
String get chat_imagePickFailed => 'Не мог да откривам та изображин';
@override
String get chat_receivedGif => 'Получил GIF';
@override @override
String get chat_reply => 'Отговори'; String get chat_reply => 'Отговори';
@@ -2037,6 +2153,42 @@ class AppLocalizationsBg extends AppLocalizations {
return 'Неуспешни изтегляния: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2586,6 +2738,42 @@ class AppLocalizationsBg extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.'; 'Байтовете, използвани за кодиране на идентификатора на този повторител в таговете за откриване на потоци/цикли, са: 0=1 байт (256 идентификатора, до 64 скока), 1=2 байта (65 000 идентификатора, до 32 скока), 2=3 байта (16 милиона идентификатора, до 21 скока). Версиите 1.13 и по-старите фърмуери използват многобайтови пътища - само след като мрежата е актуализирана до версия 1.14 или по-нова.';
@override
String get repeater_keySettings => 'Изменяне на идентичности ключа';
@override
String get repeater_keySettingsSubtitle =>
'Замени ключовият пар от публични/частни';
@override
String get repeater_prvKey => 'Частни ключ';
@override
String get repeater_prvKeyHelper =>
'Нова приватна ключ на повторчето, 128-символовна шестнадатерична строка.';
@override
String get repeater_generatePrvKey => 'Генериране на случайна ключи';
@override
String get repeater_stopGeneratingPrvKey =>
'Прервана поискна на ключова парта';
@override
String get repeater_pubKey => 'Публична ключ';
@override
String get repeater_pubKeyHelper =>
'Та е публичната ключ, която се свързува с генериранта приватна ключ. Трябва да не настройте тоа напрямо.';
@override
String get repeater_pubKeyPrefix => 'Желата префикс';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Наискате публична ключ, който почева с този шестнадатницил. Ожидавани проби: $tries.';
}
@override @override
String get repeater_txDelay => 'Забавяне на Flood TX'; String get repeater_txDelay => 'Забавяне на Flood TX';
@@ -4339,6 +4527,44 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get translation_title => 'Превод'; String get translation_title => 'Превод';
@override
String get imageMessages_enableTitle => 'Абilitiraм изображените сообщения';
@override
String get imageMessages_enableSubtitle =>
'Изправате изображения чрез сетката. Треба е одноразовна загрузка модел на изображение.';
@override
String get imageMessages_modelSectionTitle => 'Изображени модель';
@override
String get imageMessages_downloadModel => 'Скачане';
@override
String get imageMessages_cancelDownload => 'Отмена';
@override
String get imageMessages_removeModel => 'Удалете модель';
@override
String get imageMessages_modelReady => 'Готов';
@override
String get imageMessages_modelNotPublished =>
'Еще не опубликовано — тази версия не може да скане.';
@override
String get imageMessages_downloadFailed =>
'Модел за изображение не може да бъде скачан.';
@override
String get imageMessages_autoProcessTitle =>
'Автоматически обрабатвате изображения';
@override
String get imageMessages_autoProcessSubtitle =>
'Катализиране на всята изображение сразу по пришествие. Използване на около 2 ГБ на втора пъти; откажете от реконструкцията чаке при натискане.';
@override @override
String get translation_enableTitle => 'Активирайте превода'; String get translation_enableTitle => 'Активирайте превода';
@@ -4622,4 +4848,202 @@ class AppLocalizationsBg extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Спри проследяването на пакета'; String get pathMap_unfollowPacket => 'Спри проследяването на пакета';
@override
String get imageSend_title => 'Изпрати изображение';
@override
String get imageSend_cropNote =>
'Масштабиран до 512 × 512 · соотношение сторон не съхранясе';
@override
String get imageSend_originalSize => 'Късно на използване на език:';
@override
String get imageSend_onAirSize => 'В ефир';
@override
String get imageSend_quality => 'Качество';
@override
String get imageSend_qualityStandard => 'Стандарт';
@override
String get imageSend_qualityHigh => 'Высок';
@override
String get imageSend_packetsLabel => 'Пакети';
@override
String get imageSend_airtimeLabel => 'Времето на изкъсване';
@override
String get imageSend_sizeLabel => 'Баланса';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'пакета',
one: 'пакет',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'Настройки на радио неизвестни';
@override
String get imageSend_radioUnknownBody =>
'Усредете се с устройство, за да се отработа часът на изстъпане.';
@override
String get imageSend_longSendTitle => 'Долга пресилка';
@override
String imageSend_longSendBody(String duration) {
return 'Тази буде поддержива открит отряд на около $duration.';
}
@override
String get imageSend_floodNote =>
'Руйжинг на превъште: всички ретранслятори в диапазонта превратят всички пакети, тъйа каналят оставря дълше, от този час.';
@override
String get imageSend_parityTitle => 'Возобновителен пакет';
@override
String get imageSend_paritySubtitle =>
'Едно дополително пакет. Групни извещания не принимаются, след това разбирателят едно пакетто, какщо е пропущен един пакет.';
@override
String get imageSend_send => 'Отправляй';
@override
String get imageSend_cancel => 'Отмена';
@override
String get imageSend_encodeFailed =>
'Тази изображение не може да бъде кодирано.';
@override
String get imageSend_codecDownloading =>
'Модел на изображение еще разгрузира.';
@override
String get imageSend_codecUnavailable =>
'Изглед отправа не е доступен на този устройство.';
@override
String get imageSend_codecDisabled =>
'Изображенията на извещенията е отключена в настройките.';
@override
String get imageSend_deviceUnsupported =>
'Та радиота не може да пресылава изображени пакети. Подключите устройство, който работи с компанионски прошивка 13 или новия.';
@override
String get imageSend_directMessagesUnsupported =>
'Изображенията се превозводя се как групова информация, следато да се отправят единствено на канал — не в лична вестка.';
@override
String get imageSend_tooLarge =>
'Таата изображение е кодирано в по-числа пакети, отгоре както формат на сетта разрешава.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'пакета',
one: 'пакет',
);
return 'Изображението е изпратено като $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'Изображение не може да бъде отправено: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Отправка наизображение — пакет $sent от $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Узъдето $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$received от $total пакета';
}
@override
String get receivedImage_queued => 'Ожидането за декодиране';
@override
String get receivedImage_tapToDecode => 'Натискнете за декодиране';
@override
String get receivedImage_decoding => 'Повторно… за приблизително 1 секунда';
@override
String receivedImage_incomplete(int received, int total) {
return 'Изображение неполно — $received от $total пакети отправили';
}
@override
String get receivedImage_corrupt => 'Изображение не може да бъде воссоздано';
@override
String get receivedImage_decoderMissing =>
'Получена изображение — разгадка на изображение е вреjan';
@override
String get receivedImage_evicted => 'Изображение не е хранитено';
@override
String get receivedImage_retry => 'Повтори';
@override
String get receivedImage_decodeAgain => 'Правилейте заново';
@override
String get receivedImage_openSettings => 'Настройте';
@override
String get receivedImage_tapToProcess => 'Натискнете за обработка';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'пакета',
one: 'пакет',
);
return '$bytes байта · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds секунди';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes мин $seconds сек';
}
} }
+425
View File
@@ -369,6 +369,47 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert'; String get settings_radioSettingsUpdated => 'Funkparameter aktualisiert';
@override
String get settings_regionSettings => 'Regionen';
@override
String get settings_regionSettingsSubtitle =>
'Gespeicherte Regionen verwalten';
@override
String get settings_regionManagement_screenTitle => 'Regions-Verwaltung';
@override
String get settings_regionNameHint => 'Regions-Namen eingeben';
@override
String get settings_regionAddRegion => 'Region hinzufügen';
@override
String get settings_regionFetchRegions => 'Regionen von Wiederholern abrufen';
@override
String get settings_regionFetchRegionsFail =>
'Es wurden keine Regionen gefunden.';
@override
String get settings_regionFetchRegionsAlreadyExists =>
'Diese Region wurde bereits hinzugefügt';
@override
String get settings_regionName => 'Regions-Name';
@override
String get settings_regionDeleted => 'Region entfernt';
@override
String get settings_deleteRegion => 'Region entfernen';
@override
String settings_deleteRegionConfirm(String region) {
return 'Region \"$region\" aus der Liste entfernen?';
}
@override @override
String get settings_location => 'Ort'; String get settings_location => 'Ort';
@@ -466,6 +507,14 @@ class AppLocalizationsDe extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Standort in die Ankündigung einschließen.'; 'Standort in die Ankündigung einschließen.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatische Zero-Hop-Ankündigung bei GPS-Update';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Wenn sich der GPS-Standort ändert, eine Zero-Hop-Ankündigung senden (erfordert Standort in der Ankündigung).';
@override @override
String get settings_multiAck => 'Mehrfach-ACKs'; String get settings_multiAck => 'Mehrfach-ACKs';
@@ -575,6 +624,12 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Kanäle'; String get settings_infoChannelCount => 'Kanäle';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Voreinstellungen'; String get settings_presets => 'Voreinstellungen';
@@ -690,6 +745,18 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukrainisch'; String get appSettings_languageUk => 'Ukrainisch';
@override
String get repeater_pathHashModeOption0 => '0 - 1 Byte';
@override
String get repeater_pathHashModeOption1 => '1 - 2 Bytes';
@override
String get repeater_pathHashModeOption2 => '2 - 3 Bytes';
@override
String get repeater_pathHashModeOption3 => '3 - 4 Bytes';
@override @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Nachrichtenverfolgung aktivieren'; 'Nachrichtenverfolgung aktivieren';
@@ -839,6 +906,9 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,04,2V)'; String get appSettings_batteryLipo => 'LiPo (3,04,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,04,35 V)';
@override @override
String get appSettings_mapDisplay => 'Kartendarstellung'; String get appSettings_mapDisplay => 'Kartendarstellung';
@@ -896,6 +966,28 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Letzte Woche'; String get appSettings_lastWeek => 'Letzte Woche';
@override
String get appSettings_rasterTileSource => 'Quelle für Rasterkacheln';
@override
String get appSettings_stadiaEndpoint => 'Stadia-Endpunkt';
@override
String get appSettings_stadiaApiKey => 'Stadia-API-Schlüssel';
@override
String get appSettings_stadiaApiKeyRequired =>
'Für die Nutzung von Stadia Maps erforderlich';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return 'Konfiguriert: $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Geben Sie Ihren Stadia-Maps-API-Schlüssel ein. Die App verwendet ihn für Rasterkachel-Anfragen.';
@override @override
String get appSettings_offlineMapCache => 'Offline-Karten-Cache'; String get appSettings_offlineMapCache => 'Offline-Karten-Cache';
@@ -1329,6 +1421,20 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'z.B. #team'; String get channels_hashtagHint => 'z.B. #team';
@override
String channels_regionSetTo(String region) {
return 'Region: $region';
}
@override
String get channels_regionNotSet => 'Region: keine';
@override
String get channels_regionSelect_Title => 'Region auswählen';
@override
String get channels_clearRegion => 'Region zurücksetzen';
@override @override
String get chat_noMessages => 'Noch keine Nachrichten.'; String get chat_noMessages => 'Noch keine Nachrichten.';
@@ -1385,6 +1491,15 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get chat_sendGif => 'GIF senden'; String get chat_sendGif => 'GIF senden';
@override
String get chat_sendImage => 'Bild senden';
@override
String get chat_imagePickFailed => 'Konnte dieses Bild nicht öffnen.';
@override
String get chat_receivedGif => 'Erhalten eines GIF';
@override @override
String get chat_reply => 'Antworten'; String get chat_reply => 'Antworten';
@@ -2036,6 +2151,42 @@ class AppLocalizationsDe extends AppLocalizations {
return 'Fehlgeschlagene Downloads: $count'; return 'Fehlgeschlagene Downloads: $count';
} }
@override
String get mapCache_cachedTilesLabel => 'Cached tiles';
@override
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
@override
String mapCache_bulkDownloadDisabledForSource(String source) {
return 'Offline bulk downloads are disabled for $source.';
}
@override
String mapCache_bulkDownloadDisabledInConfig(String source) {
return 'Offline bulk downloads are disabled for $source in this app configuration.';
}
@override
String mapCache_summarySource(String source) {
return 'Source: $source';
}
@override
String mapCache_summaryCachedTilesForSource(int count) {
return 'Cached tiles for source: $count';
}
@override
String mapCache_summaryCachedInSelection(int count) {
return 'Cached in selected area/zoom: $count';
}
@override
String mapCache_summaryApproxCacheSize(String size) {
return 'Approx cache size: $size';
}
@override @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2581,6 +2732,43 @@ class AppLocalizationsDe extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.'; 'Bytes, die zur Kodierung der ID dieses Repeaters in Flood-Pfad-/Schleifen-Erkennung-Tags verwendet werden. 0 = 1 Byte (256 IDs, bis zu 64 Hops), 1 = 2 Bytes (65.000 IDs, bis zu 32 Hops), 2 = 3 Bytes (16 Millionen IDs, bis zu 21 Hops). Firmware-Versionen 1.13 und älter verwenden mehrstellige Pfade ab Version 1.14+ wird nur ein Pfad erstellt, sobald das Netzwerk aktiv ist.';
@override
String get repeater_keySettings => 'Identitätsschlüssel ändern';
@override
String get repeater_keySettingsSubtitle =>
'Ändern Sie das öffentliche/private Schlüsselpaar';
@override
String get repeater_prvKey => 'Privatschlüssel';
@override
String get repeater_prvKeyHelper =>
'Ein neuer privater Schlüssel für den Repeater, eine 128-stellige Hexadezimalzeichenkette.';
@override
String get repeater_generatePrvKey =>
'Generiere ein zufälliges Schlüsselpaar';
@override
String get repeater_stopGeneratingPrvKey =>
'Suchvorgang für Schlüsselpaar unterbrechen';
@override
String get repeater_pubKey => 'Öffentlicher Schlüssel';
@override
String get repeater_pubKeyHelper =>
'Dies ist der öffentliche Schlüssel, der zum generierten privaten Schlüssel gehört. Sie können dies nicht direkt festlegen.';
@override
String get repeater_pubKeyPrefix => 'Gewünschtes Präfix';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Suchen Sie einen öffentlichen Schlüssel, der mit diesen Hexadezimalziffern beginnt. Erwartete Versuche: $tries.';
}
@override @override
String get repeater_txDelay => 'Flood-TX-Verzögerung'; String get repeater_txDelay => 'Flood-TX-Verzögerung';
@@ -4347,6 +4535,43 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get translation_title => 'Übersetzung'; String get translation_title => 'Übersetzung';
@override
String get imageMessages_enableTitle => 'Bildnachrichten aktivieren';
@override
String get imageMessages_enableSubtitle =>
'Bilder über das Netzwerk senden. Einmaliges Herunterladen eines Bildmodells erforderlich.';
@override
String get imageMessages_modelSectionTitle => 'Bildmodell';
@override
String get imageMessages_downloadModel => 'Herunterladen';
@override
String get imageMessages_cancelDownload => 'Stornieren';
@override
String get imageMessages_removeModel => 'Modell entfernen';
@override
String get imageMessages_modelReady => 'Bereit';
@override
String get imageMessages_modelNotPublished =>
'Noch nicht veröffentlicht dieser Build kann es nicht herunterladen.';
@override
String get imageMessages_downloadFailed =>
'Das Bildmodell konnte nicht heruntergeladen werden.';
@override
String get imageMessages_autoProcessTitle => 'Automatisch Bilder verarbeiten';
@override
String get imageMessages_autoProcessSubtitle =>
'Rekonstruiere jedes Bild sofort nach dem Eintreffen. Verbraucht etwa 2 GB Arbeitsspeicher für jede zweite Wiederherstellung; deaktiviere die automatische Rekonstruktion mit einem Tippen.';
@override @override
String get translation_enableTitle => 'Aktivieren Sie die Übersetzung'; String get translation_enableTitle => 'Aktivieren Sie die Übersetzung';
@@ -4634,4 +4859,204 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Fixierung aufheben'; String get pathMap_unfollowPacket => 'Fixierung aufheben';
@override
String get imageSend_title => 'Bild senden';
@override
String get imageSend_cropNote =>
'Für 512 × 512 skaliert · Seitenverhältnis nicht erhalten';
@override
String get imageSend_originalSize =>
'Originaltext:\n\nThe quick brown fox jumps over the lazy dog.\n\nÜbersetzung:\n\nDer schnelle braune Fuchs springt über den faulen Hund.';
@override
String get imageSend_onAirSize => 'On air';
@override
String get imageSend_quality => 'Qualität';
@override
String get imageSend_qualityStandard => 'Standard';
@override
String get imageSend_qualityHigh => 'Hoch';
@override
String get imageSend_packetsLabel => 'Pakete';
@override
String get imageSend_airtimeLabel => 'Sendezeit';
@override
String get imageSend_sizeLabel => 'Nutzlast';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Pakete',
one: 'Paket',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'Radioeinstellungen unbekannt';
@override
String get imageSend_radioUnknownBody =>
'Verbinden Sie das Gerät, damit die Sendezeit berechnet werden kann.';
@override
String get imageSend_longSendTitle => 'Lange Übertragung';
@override
String imageSend_longSendBody(String duration) {
return 'Dies hält den Kanal für etwa $duration.';
}
@override
String get imageSend_floodNote =>
'Flutweiterleitung: Jeder Repeater im Bereich leitet jedes Paket weiter, wodurch der Kanal länger als dieser Zeitraum beschäftigt bleibt.';
@override
String get imageSend_parityTitle => 'Wiederherstellungspaket';
@override
String get imageSend_paritySubtitle =>
'Ein zusätzliches Paket. Gruppen-Nachrichten werden nicht bestätigt, daher kann der Empfänger das Bild rekonstruieren, falls ein Paket verloren geht.';
@override
String get imageSend_send => 'Senden';
@override
String get imageSend_cancel => 'Stornieren';
@override
String get imageSend_encodeFailed =>
'Dieses Bild konnte nicht kodiert werden.';
@override
String get imageSend_codecDownloading =>
'Das Bildmodell wird noch heruntergeladen.';
@override
String get imageSend_codecUnavailable =>
'Bildversand ist auf diesem Gerät nicht verfügbar.';
@override
String get imageSend_codecDisabled =>
'Bildnachrichten sind in den Einstellungen deaktiviert.';
@override
String get imageSend_deviceUnsupported =>
'Dieses Radio kann keine Bildpakete senden. Verbinden Sie ein Gerät mit der Companion-Firmware-Version 13 oder neuer.';
@override
String get imageSend_directMessagesUnsupported =>
'Bilder werden als Gruppendaten übertragen, daher können sie nur an einen Kanal gesendet werden nicht per Direktnachricht.';
@override
String get imageSend_tooLarge =>
'Dieses Bild wurde in mehr Pakete kodiert, als das Mesh-Format zulässt.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'Pakete',
one: 'Paket',
);
return 'Bild gesendet als $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'Das Bild konnte nicht gesendet werden: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Bildversand — Paket $sent von $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Kritisch: Diese Platzhalter $prefix beibehalten.';
}
@override
String receivedImage_incoming(int received, int total) {
return '$received von $total Paketen';
}
@override
String get receivedImage_queued => 'Warten auf die Entschlüsselung';
@override
String get receivedImage_tapToDecode => 'Tippen Sie, um zu entschlüsseln';
@override
String get receivedImage_decoding => 'Rekonstruktion… ca. 1 s';
@override
String receivedImage_incomplete(int received, int total) {
return 'Bild unvollständig — $received von $total Paketen eingetroffen';
}
@override
String get receivedImage_corrupt =>
'Das Bild konnte nicht rekonstruiert werden.';
@override
String get receivedImage_decoderMissing =>
'Bild empfangen — Bilddekodierung fehlgeschlagen';
@override
String get receivedImage_evicted => 'Bild nicht mehr gespeichert';
@override
String get receivedImage_retry => 'Versuchen Sie es erneut';
@override
String get receivedImage_decodeAgain => 'Entschlüsseln Sie es erneut';
@override
String get receivedImage_openSettings => 'Einrichten';
@override
String get receivedImage_tapToProcess => 'Klicken Sie, um fortzufahren';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'Pakete',
one: 'Paket',
);
return '$bytes Bytes · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds s';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes Min $seconds Sek';
}
} }
+420 -2
View File
@@ -363,6 +363,45 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Radio settings updated'; 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 @override
String get settings_location => 'Location'; String get settings_location => 'Location';
@@ -457,6 +496,14 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => 'Include location in advert.'; 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 @override
String get settings_multiAck => 'Multi-ACKs'; String get settings_multiAck => 'Multi-ACKs';
@@ -564,6 +611,12 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Channel Count'; String get settings_infoChannelCount => 'Channel Count';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Presets'; String get settings_presets => 'Presets';
@@ -679,6 +732,18 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Українська'; 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 @override
String get appSettings_enableMessageTracing => 'Enable Message Tracing'; String get appSettings_enableMessageTracing => 'Enable Message Tracing';
@@ -823,6 +888,9 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
@override @override
String get appSettings_mapDisplay => 'Map Display'; String get appSettings_mapDisplay => 'Map Display';
@@ -880,6 +948,28 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Last week'; 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 @override
String get appSettings_offlineMapCache => 'Offline Map Cache'; String get appSettings_offlineMapCache => 'Offline Map Cache';
@@ -1300,6 +1390,20 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'e.g. #team'; 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 @override
String get chat_noMessages => 'No messages yet'; String get chat_noMessages => 'No messages yet';
@@ -1355,6 +1459,15 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get chat_sendGif => 'Send GIF'; 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 @override
String get chat_reply => 'Reply'; String get chat_reply => 'Reply';
@@ -1998,6 +2111,42 @@ class AppLocalizationsEn extends AppLocalizations {
return 'Failed downloads: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2534,7 +2683,42 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Flood TX delay'; String get repeater_txDelay => 'Flood TX delay';
@@ -3006,7 +3190,7 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get repeater_cliHelpSetPrvKey => 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 @override
String get repeater_cliHelpSetRadioRxGain => String get repeater_cliHelpSetRadioRxGain =>
@@ -4268,6 +4452,43 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get translation_title => 'Translation'; 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 @override
String get translation_enableTitle => 'Enable translation'; String get translation_enableTitle => 'Enable translation';
@@ -4549,4 +4770,201 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Unlock view from packet'; 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';
}
} }
+423
View File
@@ -367,6 +367,46 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados'; String get settings_radioSettingsUpdated => 'Ajustes de radio actualizados';
@override
String get settings_regionSettings => 'Regiones';
@override
String get settings_regionSettingsSubtitle =>
'Gestionar regiones almacenadas';
@override
String get settings_regionManagement_screenTitle => 'Gestión de Regiones';
@override
String get settings_regionNameHint => 'Nombre de la región';
@override
String get settings_regionAddRegion => 'Añadir región';
@override
String get settings_regionFetchRegions => 'Obtener regiones de repetidores';
@override
String get settings_regionFetchRegionsFail => 'No se encontraron regiones';
@override
String get settings_regionFetchRegionsAlreadyExists =>
'Esta región ya ha sido agregada';
@override
String get settings_regionName => 'Nombre de la Región';
@override
String get settings_regionDeleted => 'Región eliminada';
@override
String get settings_deleteRegion => 'Eliminar Región';
@override
String settings_deleteRegionConfirm(String region) {
return '¿Eliminar \"$region\" de la lista de regiones?';
}
@override @override
String get settings_location => 'Ubicación'; String get settings_location => 'Ubicación';
@@ -464,6 +504,14 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio'; String get settings_advertLocationSubtitle => 'Incluir ubicación en anuncio';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Anuncio de cero saltos automático al actualizar GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Cuando cambie la ubicación GPS, enviar un anuncio de cero saltos (requiere ubicación en anuncio).';
@override @override
String get settings_multiAck => 'Múltiples respuestas de confirmación'; String get settings_multiAck => 'Múltiples respuestas de confirmación';
@@ -575,6 +623,12 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Número de canales'; String get settings_infoChannelCount => 'Número de canales';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Preajustes'; String get settings_presets => 'Preajustes';
@@ -690,6 +744,18 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ucraniano'; 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 @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Habilitar seguimiento de mensajes'; 'Habilitar seguimiento de mensajes';
@@ -837,6 +903,9 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
@override @override
String get appSettings_mapDisplay => 'Visualización del mapa'; String get appSettings_mapDisplay => 'Visualización del mapa';
@@ -894,6 +963,28 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'La semana pasada'; String get appSettings_lastWeek => 'La semana pasada';
@override
String get appSettings_rasterTileSource => 'Fuente de teselas ráster';
@override
String get appSettings_stadiaEndpoint => 'Punto de acceso de Stadia';
@override
String get appSettings_stadiaApiKey => 'Clave API de Stadia';
@override
String get appSettings_stadiaApiKeyRequired =>
'Obligatorio para usar Stadia Maps';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return 'Configurado: $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Introduce tu clave API de Stadia Maps. La aplicación la usa para solicitar teselas ráster.';
@override @override
String get appSettings_offlineMapCache => 'Caché de mapa sin conexión'; String get appSettings_offlineMapCache => 'Caché de mapa sin conexión';
@@ -1327,6 +1418,20 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'ej. #equipo'; String get channels_hashtagHint => 'ej. #equipo';
@override
String channels_regionSetTo(String region) {
return 'Región: $region';
}
@override
String get channels_regionNotSet => 'Región: ninguna';
@override
String get channels_regionSelect_Title => 'Seleccione una región';
@override
String get channels_clearRegion => 'Zona despejada';
@override @override
String get chat_noMessages => 'Aún no hay mensajes'; String get chat_noMessages => 'Aún no hay mensajes';
@@ -1382,6 +1487,15 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get chat_sendGif => 'Enviar GIF'; String get chat_sendGif => 'Enviar GIF';
@override
String get chat_sendImage => 'Enviar imagen';
@override
String get chat_imagePickFailed => 'No pude abrir esa imagen';
@override
String get chat_receivedGif => 'Recibido un GIF';
@override @override
String get chat_reply => 'Responder'; String get chat_reply => 'Responder';
@@ -2031,6 +2145,42 @@ class AppLocalizationsEs extends AppLocalizations {
return 'Descargas fallidas: $count'; return 'Descargas fallidas: $count';
} }
@override
String get mapCache_cachedTilesLabel => 'Cached tiles';
@override
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
@override
String mapCache_bulkDownloadDisabledForSource(String source) {
return 'Offline bulk downloads are disabled for $source.';
}
@override
String mapCache_bulkDownloadDisabledInConfig(String source) {
return 'Offline bulk downloads are disabled for $source in this app configuration.';
}
@override
String mapCache_summarySource(String source) {
return 'Source: $source';
}
@override
String mapCache_summaryCachedTilesForSource(int count) {
return 'Cached tiles for source: $count';
}
@override
String mapCache_summaryCachedInSelection(int count) {
return 'Cached in selected area/zoom: $count';
}
@override
String mapCache_summaryApproxCacheSize(String size) {
return 'Approx cache size: $size';
}
@override @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2581,6 +2731,42 @@ class AppLocalizationsEs extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.'; 'Bytes utilizados para codificar el ID de este repetidor en las etiquetas de ruta/detección de bucles. 0=1 byte (256 IDs, hasta 64 saltos), 1=2 bytes (65.000 IDs, hasta 32 saltos), 2=3 bytes (16 millones de IDs, hasta 21 saltos). Las versiones 1.13 y anteriores de firmware eliminan rutas de múltiples bytes; solo se detectan una vez que la red está activa en la versión 1.14 o posterior.';
@override
String get repeater_keySettings => 'Cambiar Claves de Identidad';
@override
String get repeater_keySettingsSubtitle =>
'Cambiar la pareja de claves pública/privada';
@override
String get repeater_prvKey => 'Clave privada';
@override
String get repeater_prvKeyHelper =>
'Una nueva clave privada para el repetidor, una cadena hexadecimal de 128 caracteres.';
@override
String get repeater_generatePrvKey => 'Genera un par de claves aleatorias';
@override
String get repeater_stopGeneratingPrvKey =>
'Interrumpir la búsqueda de par de claves';
@override
String get repeater_pubKey => 'Clave pública';
@override
String get repeater_pubKeyHelper =>
'Esta es la clave pública que corresponde a la clave privada generada. No se puede establecer directamente.';
@override
String get repeater_pubKeyPrefix => 'Prefijo deseado';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Encuentre una clave pública que comience con estos dígitos hexadecimales. Se esperan $tries intentos.';
}
@override @override
String get repeater_txDelay => 'Retraso TX por inundación'; String get repeater_txDelay => 'Retraso TX por inundación';
@@ -4343,6 +4529,44 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get translation_title => 'Traducción'; String get translation_title => 'Traducción';
@override
String get imageMessages_enableTitle => 'Mensajes de imagen';
@override
String get imageMessages_enableSubtitle =>
'Enviar imágenes a través de la malla. Requiere la descarga de un modelo de imagen único.';
@override
String get imageMessages_modelSectionTitle => 'Modelo de imagen';
@override
String get imageMessages_downloadModel => 'Descargar';
@override
String get imageMessages_cancelDownload => 'Cancelar';
@override
String get imageMessages_removeModel => 'Eliminar modelo';
@override
String get imageMessages_modelReady => 'Listo';
@override
String get imageMessages_modelNotPublished =>
'Aún no publicado — esta versión no puede descargarlo.';
@override
String get imageMessages_downloadFailed =>
'El modelo de imagen no pudo ser descargado.';
@override
String get imageMessages_autoProcessTitle =>
'Procesar imágenes automáticamente';
@override
String get imageMessages_autoProcessSubtitle =>
'Reconstruya cada imagen tan pronto como llegue. Utiliza aproximadamente 2 GB de memoria durante un segundo cada vez; desactívelo para reconstruir con un toque.';
@override @override
String get translation_enableTitle => 'Habilitar la traducción'; String get translation_enableTitle => 'Habilitar la traducción';
@@ -4628,4 +4852,203 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Dejar de seguir el paquete'; String get pathMap_unfollowPacket => 'Dejar de seguir el paquete';
@override
String get imageSend_title => 'Enviar imagen';
@override
String get imageSend_cropNote =>
'Redimensionado a 512 × 512 · la relación de aspecto no se conservó';
@override
String get imageSend_originalSize => 'Texto original';
@override
String get imageSend_onAirSize => 'En emisión';
@override
String get imageSend_quality => 'Calidad';
@override
String get imageSend_qualityStandard => 'Estándar';
@override
String get imageSend_qualityHigh => 'Alto';
@override
String get imageSend_packetsLabel => 'Paquetes';
@override
String get imageSend_airtimeLabel => 'Tiempo en pantalla';
@override
String get imageSend_sizeLabel => 'Carga útil';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'paquetes',
one: 'paquete',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle =>
'Configuración de radio desconocida';
@override
String get imageSend_radioUnknownBody =>
'Conéctese a un dispositivo para que se pueda calcular la hora de transmisión.';
@override
String get imageSend_longSendTitle => 'Larga transmisión';
@override
String imageSend_longSendBody(String duration) {
return 'Esto mantendrá el canal durante aproximadamente $duration.';
}
@override
String get imageSend_floodNote =>
'Enrutamiento de inundación: cada repetidor dentro del rango retransmite cada paquete, por lo que el canal permanece ocupado durante más tiempo que esto.';
@override
String get imageSend_parityTitle => 'Paquete de recuperación';
@override
String get imageSend_paritySubtitle =>
'Un paquete adicional. Los mensajes grupales no son reconocidos, por lo que esto permite al receptor reconstruir la imagen si se pierde un solo paquete.';
@override
String get imageSend_send => 'Enviar';
@override
String get imageSend_cancel => 'Cancelar';
@override
String get imageSend_encodeFailed => 'Esta imagen no pudo ser codificada.';
@override
String get imageSend_codecDownloading =>
'El modelo de imagen todavía se está descargando.';
@override
String get imageSend_codecUnavailable =>
'El envío de imágenes no está disponible en este dispositivo.';
@override
String get imageSend_codecDisabled =>
'Los mensajes de imagen están desactivados en la configuración.';
@override
String get imageSend_deviceUnsupported =>
'Esta radio no puede enviar paquetes de imagen. Conecte un dispositivo con el firmware Companion versión 13 o superior.';
@override
String get imageSend_directMessagesUnsupported =>
'Las imágenes se transmiten como datos grupales, por lo que solo se pueden enviar a un canal, no mediante mensajes directos.';
@override
String get imageSend_tooLarge =>
'Esa imagen se codificó en más paquetes de los que permite el formato de malla.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'paquetes',
one: 'paquete',
);
return 'Imagen enviada como $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'La imagen no pudo ser enviada: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Envío de imagen — paquete $sent de $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Nodo $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$received de $total paquetes';
}
@override
String get receivedImage_queued => 'Esperando descifrar';
@override
String get receivedImage_tapToDecode => 'Toque para decodificar';
@override
String get receivedImage_decoding =>
'Reconstruyendo… aproximadamente 1 segundo';
@override
String receivedImage_incomplete(int received, int total) {
return 'Imagen incompleta — $received de $total paquetes llegados';
}
@override
String get receivedImage_corrupt => 'La imagen no pudo ser reconstruida';
@override
String get receivedImage_decoderMissing =>
'Imagen recibida — la decodificación de la imagen no funciona';
@override
String get receivedImage_evicted => 'Imagen ya no almacenada';
@override
String get receivedImage_retry => 'Inténtalo de nuevo';
@override
String get receivedImage_decodeAgain => 'Decodificar de nuevo';
@override
String get receivedImage_openSettings => 'Configurar';
@override
String get receivedImage_tapToProcess => 'Toque para procesar';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'paquetes',
one: 'paquete',
);
return '$bytes bytes · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds segundos';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes min $seconds s';
}
} }
+424
View File
@@ -369,6 +369,47 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour'; String get settings_radioSettingsUpdated => 'Paramètres radio mis à jour';
@override
String get settings_regionSettings => 'Régions';
@override
String get settings_regionSettingsSubtitle => 'Gérer les régions stockées';
@override
String get settings_regionManagement_screenTitle => 'Gestion de région';
@override
String get settings_regionNameHint => 'Entrer le nom de la région';
@override
String get settings_regionAddRegion => 'Ajouter une région';
@override
String get settings_regionFetchRegions =>
'Récupérer les régions à partir des répéteurs';
@override
String get settings_regionFetchRegionsFail =>
'Aucune région n\'a été trouvée';
@override
String get settings_regionFetchRegionsAlreadyExists =>
'Cette région a déjà été ajoutée';
@override
String get settings_regionName => 'Nom de la région';
@override
String get settings_regionDeleted => 'Région supprimée';
@override
String get settings_deleteRegion => 'Supprimer la région';
@override
String settings_deleteRegionConfirm(String region) {
return 'Supprimez $region de la liste des régions ?';
}
@override @override
String get settings_location => 'Emplacement'; String get settings_location => 'Emplacement';
@@ -468,6 +509,14 @@ class AppLocalizationsFr extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Inclure la localisation dans l\'annonce'; 'Inclure la localisation dans l\'annonce';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Annonce zéro saut automatique lors de la mise à jour GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Lorsque la position GPS change, envoyer une annonce zéro saut (nécessite la position dans l\'annonce).';
@override @override
String get settings_multiAck => 'Plusieurs accusés de réception'; String get settings_multiAck => 'Plusieurs accusés de réception';
@@ -579,6 +628,12 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Nombre de canaux'; String get settings_infoChannelCount => 'Nombre de canaux';
@override
String get settings_infoHardware => 'Matériel';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Préréglages'; String get settings_presets => 'Préréglages';
@@ -694,6 +749,18 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukrainien'; String get appSettings_languageUk => 'Ukrainien';
@override
String get repeater_pathHashModeOption0 => '0 - 1 octet';
@override
String get repeater_pathHashModeOption1 => '1 à 2 octets';
@override
String get repeater_pathHashModeOption2 => '2 à 3 octets';
@override
String get repeater_pathHashModeOption3 => '3 à 4 octets';
@override @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Activer le traçage des messages'; 'Activer le traçage des messages';
@@ -841,6 +908,9 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'Tension élevée LiPo (3,0-4,35 V)';
@override @override
String get appSettings_mapDisplay => 'Affichage de la carte'; String get appSettings_mapDisplay => 'Affichage de la carte';
@@ -898,6 +968,28 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'La semaine dernière'; String get appSettings_lastWeek => 'La semaine dernière';
@override
String get appSettings_rasterTileSource => 'Source de tuiles raster';
@override
String get appSettings_stadiaEndpoint => 'Point de terminaison Stadia';
@override
String get appSettings_stadiaApiKey => 'Clé API Stadia';
@override
String get appSettings_stadiaApiKeyRequired =>
'Requis pour lutilisation de Stadia Maps';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return 'Configuré : $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Entrez votre clé API Stadia Maps. Cette application lutilise pour les requêtes de tuiles raster.';
@override @override
String get appSettings_offlineMapCache => 'Cache de carte hors ligne'; String get appSettings_offlineMapCache => 'Cache de carte hors ligne';
@@ -1329,6 +1421,20 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'ex. #equipe'; String get channels_hashtagHint => 'ex. #equipe';
@override
String channels_regionSetTo(String region) {
return 'Région : $region';
}
@override
String get channels_regionNotSet => 'Région : aucune';
@override
String get channels_regionSelect_Title => 'Choisissez une région';
@override
String get channels_clearRegion => 'Zone dégagée';
@override @override
String get chat_noMessages => 'Aucun message pour le moment'; String get chat_noMessages => 'Aucun message pour le moment';
@@ -1384,6 +1490,15 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get chat_sendGif => 'Envoyer un GIF'; String get chat_sendGif => 'Envoyer un GIF';
@override
String get chat_sendImage => 'Envoyer l\'image';
@override
String get chat_imagePickFailed => 'Je n\'ai pas pu ouvrir cette image';
@override
String get chat_receivedGif => 'Reçu un GIF';
@override @override
String get chat_reply => 'Répondre'; String get chat_reply => 'Répondre';
@@ -2038,6 +2153,42 @@ class AppLocalizationsFr extends AppLocalizations {
return 'Téléchargements échoués : $count'; return 'Téléchargements échoués : $count';
} }
@override
String get mapCache_cachedTilesLabel => 'Cached tiles';
@override
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
@override
String mapCache_bulkDownloadDisabledForSource(String source) {
return 'Offline bulk downloads are disabled for $source.';
}
@override
String mapCache_bulkDownloadDisabledInConfig(String source) {
return 'Offline bulk downloads are disabled for $source in this app configuration.';
}
@override
String mapCache_summarySource(String source) {
return 'Source: $source';
}
@override
String mapCache_summaryCachedTilesForSource(int count) {
return 'Cached tiles for source: $count';
}
@override
String mapCache_summaryCachedInSelection(int count) {
return 'Cached in selected area/zoom: $count';
}
@override
String mapCache_summaryApproxCacheSize(String size) {
return 'Approx cache size: $size';
}
@override @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2592,6 +2743,42 @@ class AppLocalizationsFr extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.'; 'Octets utilisés pour encoder l\'ID de ce routeur dans les balises de détection de flux/boucles. 0 = 1 octet (256 ID, jusqu\'à 64 sauts), 1 = 2 octets (65 000 ID, jusqu\'à 32 sauts), 2 = 3 octets (16 millions d\'ID, jusqu\'à 21 sauts). Les versions 1.13 et antérieures utilisent des chemins multi-octets ; à partir de la version 1.14, cela n\'est plus nécessaire.';
@override
String get repeater_keySettings => 'Modifier les clés d\'identité';
@override
String get repeater_keySettingsSubtitle =>
'Modifier la paire de clés publique/privée';
@override
String get repeater_prvKey => 'Clé privée';
@override
String get repeater_prvKeyHelper =>
'Une nouvelle clé privée pour le répéteur, une chaîne hexadécimale de 128 caractères.';
@override
String get repeater_generatePrvKey => 'Générez une paire de clés aléatoire';
@override
String get repeater_stopGeneratingPrvKey =>
'Arrêter la recherche de la paire de clés';
@override
String get repeater_pubKey => 'Clé publique';
@override
String get repeater_pubKeyHelper =>
'Voici la clé publique associée à la clé privée générée. Vous ne pouvez pas la définir directement.';
@override
String get repeater_pubKeyPrefix => 'Préfixe souhaité';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Trouvez une clé publique qui commence par ces chiffres hexadécimaux. Nombre de tentatives nécessaires : $tries.';
}
@override @override
String get repeater_txDelay => 'Délai de transmission en flood'; String get repeater_txDelay => 'Délai de transmission en flood';
@@ -4364,6 +4551,44 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get translation_title => 'Traduction'; String get translation_title => 'Traduction';
@override
String get imageMessages_enableTitle => 'Messages avec images';
@override
String get imageMessages_enableSubtitle =>
'Envoyer des images via le maillage. Nécessite un téléchargement unique du modèle d\'image.';
@override
String get imageMessages_modelSectionTitle => 'Modèle d\'image';
@override
String get imageMessages_downloadModel => 'Télécharger';
@override
String get imageMessages_cancelDownload => 'Annuler';
@override
String get imageMessages_removeModel => 'Supprimer le modèle';
@override
String get imageMessages_modelReady => 'Prêt';
@override
String get imageMessages_modelNotPublished =>
'Non publié — cette version ne peut pas le télécharger.';
@override
String get imageMessages_downloadFailed =>
'Le modèle d\'image ne pouvait pas être téléchargé.';
@override
String get imageMessages_autoProcessTitle =>
'Traiter automatiquement les images';
@override
String get imageMessages_autoProcessSubtitle =>
'Reconstruisez chaque image dès son arrivée. Cela utilise environ 2 Go de mémoire pendant une seconde à chaque fois ; désactivez la reconstruction au toucher pour éviter cela.';
@override @override
String get translation_enableTitle => 'Activer la traduction'; String get translation_enableTitle => 'Activer la traduction';
@@ -4649,4 +4874,203 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Déverrouiller la vue du paquet'; String get pathMap_unfollowPacket => 'Déverrouiller la vue du paquet';
@override
String get imageSend_title => 'Envoyer l\'image';
@override
String get imageSend_cropNote =>
'Redimensionné à 512 × 512 · le rapport daspect na pas été conservé';
@override
String get imageSend_originalSize =>
'Texte original\n\nVeuillez traduire le texte suivant en français :';
@override
String get imageSend_onAirSize => 'En direct';
@override
String get imageSend_quality => 'Qualité';
@override
String get imageSend_qualityStandard => 'Standard';
@override
String get imageSend_qualityHigh => 'Haut';
@override
String get imageSend_packetsLabel => 'Paquets';
@override
String get imageSend_airtimeLabel => 'Durée de diffusion';
@override
String get imageSend_sizeLabel => 'Charge utile';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'paquets',
one: 'paquet',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'Réglages de la radio inconnus';
@override
String get imageSend_radioUnknownBody =>
'Connectez-vous à un appareil afin que l\'heure de diffusion puisse être calculée.';
@override
String get imageSend_longSendTitle => 'Longue transmission';
@override
String imageSend_longSendBody(String duration) {
return 'Cela maintiendra la chaîne pendant environ $duration.';
}
@override
String get imageSend_floodNote =>
'Routage des inondations : chaque répéteur dans la zone retransmet chaque paquet, de sorte que le canal reste occupé plus longtemps que cela.';
@override
String get imageSend_parityTitle => 'Paquet de récupération';
@override
String get imageSend_paritySubtitle =>
'Un paquet supplémentaire. Les messages de groupe ne sont pas reconnus, ce qui permet au destinataire de reconstruire l\'image si un seul paquet est perdu.';
@override
String get imageSend_send => 'Envoyer';
@override
String get imageSend_cancel => 'Annuler';
@override
String get imageSend_encodeFailed => 'Cette image ne peut pas être encodée.';
@override
String get imageSend_codecDownloading =>
'Le modèle d\'image est toujours en téléchargement.';
@override
String get imageSend_codecUnavailable =>
'L\'envoi d\'images n\'est pas disponible sur cet appareil.';
@override
String get imageSend_codecDisabled =>
'Les messages d\'images sont désactivés dans les paramètres.';
@override
String get imageSend_deviceUnsupported =>
'Cette radio ne peut pas envoyer de paquets d\'images. Connectez un appareil exécutant le firmware compagnon version 13 ou ultérieure.';
@override
String get imageSend_directMessagesUnsupported =>
'Les images se déplacent en tant que données de groupe, elles ne peuvent donc être envoyées que sur un canal — et non par message direct.';
@override
String get imageSend_tooLarge =>
'Cette image a été encodée en plus de paquets que le format de maillage ne permet.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'paquets',
one: 'paquet',
);
return 'Image envoyée en $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'L\'image n\'a pas pu être envoyée : $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Envoi de l\'image — paquet $sent sur $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Nœud $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$received sur $total paquets';
}
@override
String get receivedImage_queued => 'En attendant de décoder';
@override
String get receivedImage_tapToDecode => 'Tapez pour décoder';
@override
String get receivedImage_decoding => 'Reconstitution… denviron 1 s';
@override
String receivedImage_incomplete(int received, int total) {
return 'Image incomplète — $received sur $total paquets reçus';
}
@override
String get receivedImage_corrupt =>
'L\'image ne pouvait pas être reconstruite';
@override
String get receivedImage_decoderMissing =>
'Image reçue — le décodage de l\'image est interrompu';
@override
String get receivedImage_evicted => 'Image non stockée';
@override
String get receivedImage_retry => 'Réessayez';
@override
String get receivedImage_decodeAgain => 'Décoder à nouveau';
@override
String get receivedImage_openSettings => 'Mettre en place';
@override
String get receivedImage_tapToProcess => 'Tapez pour traiter';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'paquets',
one: 'paquet',
);
return '$bytes octets · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds s';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes minutes $seconds secondes';
}
} }
+420
View File
@@ -365,6 +365,46 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve'; String get settings_radioSettingsUpdated => 'A rádió beállításai frissítve';
@override
String get settings_regionSettings => 'Tájak';
@override
String get settings_regionSettingsSubtitle =>
'Kincstani megjelenítette gyakorlatos területeket';
@override
String get settings_regionManagement_screenTitle => 'Regionális Kezelés';
@override
String get settings_regionNameHint => 'Felhasználja a regioni név';
@override
String get settings_regionAddRegion => 'Egyes region';
@override
String get settings_regionFetchRegions => 'Féregelmezők a repeaterekból';
@override
String get settings_regionFetchRegionsFail => 'Nem található egyéb területek';
@override
String get settings_regionFetchRegionsAlreadyExists =>
'Ez a terítet nemisértették meg.';
@override
String get settings_regionName => 'Feldúr Neve';
@override
String get settings_regionDeleted => 'Felült region';
@override
String get settings_deleteRegion => 'Felajánlatos Tér';
@override
String settings_deleteRegionConfirm(String region) {
return 'Kell kiszámítsd a ${region}ot a részekre?';
}
@override @override
String get settings_location => 'hely'; String get settings_location => 'hely';
@@ -463,6 +503,14 @@ class AppLocalizationsHu extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'A hirdetésben szerepeljen a helyszín.'; 'A hirdetésben szerepeljen a helyszín.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Automatikus zero-hop hirdetés GPS-frissítéskor';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Ha a GPS-helyzet megváltozik, küldjön zero-hop hirdetést (a hirdetésben helymegadás szükséges).';
@override @override
String get settings_multiAck => 'Multi-ACK'; String get settings_multiAck => 'Multi-ACK';
@@ -572,6 +620,12 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Csatornaszám'; String get settings_infoChannelCount => 'Csatornaszám';
@override
String get settings_infoHardware => 'Hardverő';
@override
String get settings_infoFirmware => 'Firmver';
@override @override
String get settings_presets => 'Előbeállítások'; String get settings_presets => 'Előbeállítások';
@@ -689,6 +743,18 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Українська'; String get appSettings_languageUk => 'Українська';
@override
String get repeater_pathHashModeOption0 => '0 - 1 byte';
@override
String get repeater_pathHashModeOption1 => '1 - 2 szavak';
@override
String get repeater_pathHashModeOption2 => '2 - 3 bajt';
@override
String get repeater_pathHashModeOption3 => '3 - 4 bajt';
@override @override
String get appSettings_enableMessageTracing => 'Üzenetkövetés engedélyezése'; String get appSettings_enableMessageTracing => 'Üzenetkövetés engedélyezése';
@@ -832,6 +898,9 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2 V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
@override @override
String get appSettings_mapDisplay => 'Térkép megjelenítése'; String get appSettings_mapDisplay => 'Térkép megjelenítése';
@@ -890,6 +959,28 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Múlt héten'; String get appSettings_lastWeek => 'Múlt héten';
@override
String get appSettings_rasterTileSource => 'Raszteres csempeforrás';
@override
String get appSettings_stadiaEndpoint => 'Stadia végpont';
@override
String get appSettings_stadiaApiKey => 'Stadia API-kulcs';
@override
String get appSettings_stadiaApiKeyRequired =>
'A Stadia Maps használatához kötelező';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return 'Konfigurálva: $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Adja meg a Stadia Maps API-kulcsát. Az alkalmazás ezt használja raszteres csempekérésekhez.';
@override @override
String get appSettings_offlineMapCache => 'Offline térképgyorsítótár'; String get appSettings_offlineMapCache => 'Offline térképgyorsítótár';
@@ -1322,6 +1413,20 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'például #csapat'; String get channels_hashtagHint => 'például #csapat';
@override
String channels_regionSetTo(String region) {
return 'Tájékoztatás: A $region -t\n\nEz a tájékoztatás a magyar nyelvben a következően kell szóva:\n\nTájékoztatás: A $region -t';
}
@override
String get channels_regionNotSet => 'Tájékoztetés: nem';
@override
String get channels_regionSelect_Title => 'Válassz egy regionot';
@override
String get channels_clearRegion => 'Kéres kör';
@override @override
String get chat_noMessages => 'Még nincsenek üzenetek'; String get chat_noMessages => 'Még nincsenek üzenetek';
@@ -1377,6 +1482,15 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get chat_sendGif => 'GIF küldése'; String get chat_sendGif => 'GIF küldése';
@override
String get chat_sendImage => 'Felhasználj az image';
@override
String get chat_imagePickFailed => 'Nem tudtam ezt a fenti fényre kapni';
@override
String get chat_receivedGif => 'GIF-ot kapott';
@override @override
String get chat_reply => 'Válasz'; String get chat_reply => 'Válasz';
@@ -2029,6 +2143,42 @@ class AppLocalizationsHu extends AppLocalizations {
return 'Sikertelen letöltések: $count'; return 'Sikertelen letöltések: $count';
} }
@override
String get mapCache_cachedTilesLabel => 'Cached tiles';
@override
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
@override
String mapCache_bulkDownloadDisabledForSource(String source) {
return 'Offline bulk downloads are disabled for $source.';
}
@override
String mapCache_bulkDownloadDisabledInConfig(String source) {
return 'Offline bulk downloads are disabled for $source in this app configuration.';
}
@override
String mapCache_summarySource(String source) {
return 'Source: $source';
}
@override
String mapCache_summaryCachedTilesForSource(int count) {
return 'Cached tiles for source: $count';
}
@override
String mapCache_summaryCachedInSelection(int count) {
return 'Cached in selected area/zoom: $count';
}
@override
String mapCache_summaryApproxCacheSize(String size) {
return 'Approx cache size: $size';
}
@override @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2575,6 +2725,42 @@ class AppLocalizationsHu extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.'; 'Az átjátszó azonosítójának elárasztási útvonal/hurokészlelési címkékbe való kódolására használt bájtok. 0 = 1 bájt (256 azonosító, legfeljebb 64 ugrás), 1 = 2 bájt (65 000 azonosító, legfeljebb 32 ugrás), 2 = 3 bájt (16 millió azonosító, legfeljebb 21 ugrás). A v1.13 és régebbi firmware eldobja a többbájtos elérési utat csak akkor emelje meg, ha a hálózat a v1.14+ verziót használja.';
@override
String get repeater_keySettings => 'Képzőítsekei jelölt';
@override
String get repeater_keySettingsSubtitle =>
'Közelítse a közlekedési/private képlékt';
@override
String get repeater_prvKey => 'Kis házas';
@override
String get repeater_prvKeyHelper =>
'Kínai nyelvben:\n\nA repeteróhoz új képes kód, 128 karakteri hex-kód.';
@override
String get repeater_generatePrvKey => 'Egy random képléktár generáld meg';
@override
String get repeater_stopGeneratingPrvKey =>
'Kéypair szavakat kérdéset kaphatás';
@override
String get repeater_pubKey => 'Kiváló kiegészítő';
@override
String get repeater_pubKeyHelper =>
'Ez a kiváló kód, amely tükrözi a gyártott kiegészítő kódot. Ezet nem lehet megjelenítani direktusan.';
@override
String get repeater_pubKeyPrefix => 'Kiváló prefix';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Kiválasztsa meg a közös kódot, amely az alábbi hex-kifüggőnek előre van. Várható kiválasztások száma: $tries.';
}
@override @override
String get repeater_txDelay => 'Áradási TX késleltetés'; String get repeater_txDelay => 'Áradási TX késleltetés';
@@ -4337,6 +4523,43 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get translation_title => 'Fordítás'; String get translation_title => 'Fordítás';
@override
String get imageMessages_enableTitle => 'Fenti hírüket biztosítani';
@override
String get imageMessages_enableSubtitle =>
'Fényokat a hálózatban küldd el. Egyértelmű fénymodell hozzáadásra kell.';
@override
String get imageMessages_modelSectionTitle => 'Image model';
@override
String get imageMessages_downloadModel => 'Hozzáladás';
@override
String get imageMessages_cancelDownload => 'Színviselés';
@override
String get imageMessages_removeModel => 'Kisnövezdés';
@override
String get imageMessages_modelReady => 'Hozzá';
@override
String get imageMessages_modelNotPublished =>
'Érkeztetve nem — ez a építési módszert nem lehet lejobbítani.';
@override
String get imageMessages_downloadFailed => 'Életmódelmát nem tudtam hozni.';
@override
String get imageMessages_autoProcessTitle =>
'Automatikusan kínaltatja fénybetek';
@override
String get imageMessages_autoProcessSubtitle =>
'Minden fenti image-t kiválasztsa meg, amikor érkezik. Minden más esetben 2 GB fényre kell szükséges; megoldja a rekonstruálisi a támogatásra egy klikkel.';
@override @override
String get translation_enableTitle => 'Fordítás engedélyezése'; String get translation_enableTitle => 'Fordítás engedélyezése';
@@ -4620,4 +4843,201 @@ class AppLocalizationsHu extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'A nézet feloldása a csomagból'; String get pathMap_unfollowPacket => 'A nézet feloldása a csomagból';
@override
String get imageSend_title => 'Felhasználj az image';
@override
String get imageSend_cropNote =>
'512 × 512 órák számára megosztva · képszöget nem biztosítva';
@override
String get imageSend_originalSize => 'Következtetés:';
@override
String get imageSend_onAirSize => 'Erőben';
@override
String get imageSend_quality => 'Hosszú Képesség';
@override
String get imageSend_qualityStandard => 'Standard';
@override
String get imageSend_qualityHigh => 'High: Magas';
@override
String get imageSend_packetsLabel => 'Paketek';
@override
String get imageSend_airtimeLabel => 'Kiválasztás a fent';
@override
String get imageSend_sizeLabel => 'Kártya';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'csomag',
one: 'csomag',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'Radiooldalok nem tudom';
@override
String get imageSend_radioUnknownBody =>
'Kizárdhoz kapcsolódd a rendszert, hogy a súly az az álló időt lehet elismerni.';
@override
String get imageSend_longSendTitle => 'Uzun hírás';
@override
String imageSend_longSendBody(String duration) {
return 'Ez megbízható a kanal $duration-n.';
}
@override
String get imageSend_floodNote =>
'Fúró irányozás: Minden repeater a fúró körüléggel megköveti mindkét paketet, ezért a kanal eltartja más a lehetséges időt.';
@override
String get imageSend_parityTitle => 'Korzolás paksa';
@override
String get imageSend_paritySubtitle =>
'Egy kibőbbes kapszula. Gruppos hírselek nem érkezik, ezért az empfangó nem kell elismerni a jel, ha egy egyes kapszula kaland, hogy nem teljesen érkezz.';
@override
String get imageSend_send => 'Keresés';
@override
String get imageSend_cancel => 'Színviselés';
@override
String get imageSend_encodeFailed => 'Ez a fényet nem tudtam kódolni.';
@override
String get imageSend_codecDownloading =>
'Érdekesz az image model megstálása még van.';
@override
String get imageSend_codecUnavailable =>
'Felhasználó észintje nem kapható ezen a rendszert.';
@override
String get imageSend_codecDisabled =>
'Image hírletek belsőoldal elkalmáznak a konfigurációban.';
@override
String get imageSend_deviceUnsupported =>
'Ez a radio nem tudja verseni színekfájltakat. Habilítd a kompatibilis firmware 13 vagy újabb versionot kapcsált eszközet.';
@override
String get imageSend_directMessagesUnsupported =>
'Fényiak gyakorolják grupos adatban, ezért nem lehetek versenek egy hozzáfűzésre, csak egy kanalra.';
@override
String get imageSend_tooLarge =>
'A figyelmetes fentet nem kínálhat a körközlettel ismétálás elterjesztésére.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'csomagként',
one: 'csomagként',
);
return 'Kép elküldve $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'Fentami nem tűnik: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Felhasználó: Lehet a következőkét kifejezetet $sent paket $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Node $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return 'Kulcsfontosságú: Kiválasztja meg a $received $total paketot';
}
@override
String get receivedImage_queued => 'Kódoláshoz küldőzve';
@override
String get receivedImage_tapToDecode => 'Kódoláshoz klikd el';
@override
String get receivedImage_decoding => 'Korlátozás… egy 1 perc után';
@override
String receivedImage_incomplete(int received, int total) {
return 'Kérem, az $received $total paketet kapottak.';
}
@override
String get receivedImage_corrupt => 'Fény nem tudtam megrealítani';
@override
String get receivedImage_decoderMissing =>
'Kezd az image — image decodálás nem kapható';
@override
String get receivedImage_evicted => 'Fény nem tartalmazva';
@override
String get receivedImage_retry => 'Kérem kell próbálni';
@override
String get receivedImage_decodeAgain => 'Kodoldó';
@override
String get receivedImage_openSettings => 'Jelzése';
@override
String get receivedImage_tapToProcess => 'Klikd a feljelenítést';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'csomag',
one: 'csomag',
);
return '$bytes bájt · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds sekünd\n\n(Note: The placeholder $seconds is kept unchanged as requested.)';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes per $seconds sekünd\n\n(Note: The original text appears to be a placeholder format rather than a complete sentence. The translation reflects the literal structure while preserving the placeholders as requested.)';
}
} }
+428 -1
View File
@@ -369,6 +369,48 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate'; String get settings_radioSettingsUpdated => 'Impostazioni radio aggiornate';
@override
String get settings_regionSettings => 'Regioni';
@override
String get settings_regionSettingsSubtitle =>
'Gestione delle regioni salvate';
@override
String get settings_regionManagement_screenTitle => 'Gestione Regionale';
@override
String get settings_regionNameHint => 'Inserisci il nome della regione';
@override
String get settings_regionAddRegion => 'Aggiungi regione';
@override
String get settings_regionFetchRegions =>
'Recupera le regioni dai ripetitori';
@override
String get settings_regionFetchRegionsFail =>
'Non sono state trovate regioni';
@override
String get settings_regionFetchRegionsAlreadyExists =>
'Questa regione è già stata aggiunta';
@override
String get settings_regionName => 'Nome della Regione';
@override
String get settings_regionDeleted => 'Regione cancellata';
@override
String get settings_deleteRegion => 'Elimina Regione';
@override
String settings_deleteRegionConfirm(String region) {
return 'Rimuovere \"$region\" dalla lista delle regioni?';
}
@override @override
String get settings_location => 'Posizione'; String get settings_location => 'Posizione';
@@ -468,6 +510,14 @@ class AppLocalizationsIt extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Includi la posizione nell\'annuncio'; 'Includi la posizione nell\'annuncio';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Annuncio zero-hop automatico all\'aggiornamento GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Quando la posizione GPS cambia, invia un annuncio zero-hop (richiede la posizione nell\'annuncio).';
@override @override
String get settings_multiAck => 'ACK multipli'; String get settings_multiAck => 'ACK multipli';
@@ -577,6 +627,12 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Numero di canali'; String get settings_infoChannelCount => 'Numero di canali';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Preset'; String get settings_presets => 'Preset';
@@ -692,6 +748,18 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ucraino'; String get appSettings_languageUk => 'Ucraino';
@override
String get repeater_pathHashModeOption0 => '0 - 1 byte';
@override
String get repeater_pathHashModeOption1 => '1 - 2 byte';
@override
String get repeater_pathHashModeOption2 => '2 - 3 byte';
@override
String get repeater_pathHashModeOption3 => '3 - 4 byte';
@override @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Abilita tracciamento messaggi'; 'Abilita tracciamento messaggi';
@@ -840,6 +908,9 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
@override @override
String get appSettings_mapDisplay => 'Visualizzazione Mappa'; String get appSettings_mapDisplay => 'Visualizzazione Mappa';
@@ -897,6 +968,28 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'La settimana scorsa'; String get appSettings_lastWeek => 'La settimana scorsa';
@override
String get appSettings_rasterTileSource => 'Sorgente tile raster';
@override
String get appSettings_stadiaEndpoint => 'Endpoint Stadia';
@override
String get appSettings_stadiaApiKey => 'Chiave API Stadia';
@override
String get appSettings_stadiaApiKeyRequired =>
'Obbligatoria per usare Stadia Maps';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return 'Configurata: $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Inserisci la chiave API di Stadia Maps. Questa app la usa per le richieste di tile raster.';
@override @override
String get appSettings_offlineMapCache => 'Cache Mappa Offline'; String get appSettings_offlineMapCache => 'Cache Mappa Offline';
@@ -1328,6 +1421,20 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'es. #team'; String get channels_hashtagHint => 'es. #team';
@override
String channels_regionSetTo(String region) {
return 'Regione: $region';
}
@override
String get channels_regionNotSet => 'Regione: nessuna';
@override
String get channels_regionSelect_Title => 'Scegli una regione';
@override
String get channels_clearRegion => 'Zona chiara';
@override @override
String get chat_noMessages => 'Nessun messaggio ancora'; String get chat_noMessages => 'Nessun messaggio ancora';
@@ -1383,6 +1490,16 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get chat_sendGif => 'Invia GIF'; String get chat_sendGif => 'Invia GIF';
@override
String get chat_sendImage => 'Invia immagine';
@override
String get chat_imagePickFailed =>
'Non sono riuscito ad aprire quell\'immagine';
@override
String get chat_receivedGif => 'Ricevuto un GIF';
@override @override
String get chat_reply => 'Rispondi'; String get chat_reply => 'Rispondi';
@@ -2033,6 +2150,42 @@ class AppLocalizationsIt extends AppLocalizations {
return 'Download falliti: $count'; return 'Download falliti: $count';
} }
@override
String get mapCache_cachedTilesLabel => 'Cached tiles';
@override
String get mapCache_cachedTileSummaryLabel => 'Cached tile summary';
@override
String mapCache_bulkDownloadDisabledForSource(String source) {
return 'Offline bulk downloads are disabled for $source.';
}
@override
String mapCache_bulkDownloadDisabledInConfig(String source) {
return 'Offline bulk downloads are disabled for $source in this app configuration.';
}
@override
String mapCache_summarySource(String source) {
return 'Source: $source';
}
@override
String mapCache_summaryCachedTilesForSource(int count) {
return 'Cached tiles for source: $count';
}
@override
String mapCache_summaryCachedInSelection(int count) {
return 'Cached in selected area/zoom: $count';
}
@override
String mapCache_summaryApproxCacheSize(String size) {
return 'Approx cache size: $size';
}
@override @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2581,7 +2734,43 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag per il rilevamento del percorso/loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Le versioni 1.13 e precedenti utilizzano percorsi multi-byte: è necessario attivare la rete prima di utilizzare questa funzionalità (a partire dalla versione 1.14).'; 'Byte utilizzati per codificare l\'ID di questo ripetitore nei tag di percorso flood/rilevamento loop. 0=1 byte (256 ID, fino a 64 salti), 1=2 byte (65.000 ID, fino a 32 salti), 2=3 byte (16 milioni di ID, fino a 21 salti). Il firmware precedente alla v1.14 usava sempre percorsi a 1 byte; v1.14 e versioni successive possono essere configurate per percorsi a 2 o 3 byte.';
@override
String get repeater_keySettings => 'Cambia le Chiavi di Identità';
@override
String get repeater_keySettingsSubtitle =>
'Cambia la coppia di chiavi pubblica/privata';
@override
String get repeater_prvKey => 'Chiave privata';
@override
String get repeater_prvKeyHelper =>
'Una nuova chiave privata per il ripetitore, una stringa esadecimale di 128 caratteri.';
@override
String get repeater_generatePrvKey => 'Genera una coppia di chiavi casuale';
@override
String get repeater_stopGeneratingPrvKey =>
'Interrompi la ricerca della coppia di chiavi';
@override
String get repeater_pubKey => 'Chiave pubblica';
@override
String get repeater_pubKeyHelper =>
'Questa è la chiave pubblica che accompagna la chiave privata generata. Non è possibile impostarla direttamente.';
@override
String get repeater_pubKeyPrefix => 'Prefisso desiderato';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'Trova una chiave pubblica che inizia con queste cifre esadecimali. Tentativi necessari previsti: $tries.';
}
@override @override
String get repeater_txDelay => 'Ritardo a Flood, TX'; String get repeater_txDelay => 'Ritardo a Flood, TX';
@@ -4347,6 +4536,44 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get translation_title => 'Traduzione'; String get translation_title => 'Traduzione';
@override
String get imageMessages_enableTitle => 'Messaggi con immagini';
@override
String get imageMessages_enableSubtitle =>
'Invia le immagini tramite la mesh. È necessario scaricare il modello dell\'immagine una tantum.';
@override
String get imageMessages_modelSectionTitle => 'Modello di immagine';
@override
String get imageMessages_downloadModel => 'Scarica';
@override
String get imageMessages_cancelDownload => 'Annullare';
@override
String get imageMessages_removeModel => 'Rimuovi il modello';
@override
String get imageMessages_modelReady => 'Pronto';
@override
String get imageMessages_modelNotPublished =>
'Non pubblicato ancora — questa versione non può scaricarlo.';
@override
String get imageMessages_downloadFailed =>
'Il modello di immagine non può essere scaricato.';
@override
String get imageMessages_autoProcessTitle =>
'Elabora automaticamente le immagini';
@override
String get imageMessages_autoProcessSubtitle =>
'Ricostruisci ogni immagine non appena arriva. Utilizza circa 2 GB di memoria per un secondo ogni volta; disattiva la ricostruzione con un semplice tocco.';
@override @override
String get translation_enableTitle => 'Abilitare la traduzione'; String get translation_enableTitle => 'Abilitare la traduzione';
@@ -4633,4 +4860,204 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Sblocca la vista dal pacchetto'; String get pathMap_unfollowPacket => 'Sblocca la vista dal pacchetto';
@override
String get imageSend_title => 'Invia immagine';
@override
String get imageSend_cropNote =>
'Ridimensionato a 512 × 512 · rapporto d\'aspetto non conservato';
@override
String get imageSend_originalSize =>
'Traduzione:\n\nOriginal text:\n\nPlease translate the following English text into Italian:\n\nOriginal text:\n\n(No original English text provided)';
@override
String get imageSend_onAirSize => 'In onda';
@override
String get imageSend_quality => 'Qualità';
@override
String get imageSend_qualityStandard => 'Standard';
@override
String get imageSend_qualityHigh => 'Alto';
@override
String get imageSend_packetsLabel => 'Pacchetti';
@override
String get imageSend_airtimeLabel => 'Tempo in onda';
@override
String get imageSend_sizeLabel => 'Carico utile';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'pacchetti',
one: 'pacchetto',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'Impostazioni radio sconosciute';
@override
String get imageSend_radioUnknownBody =>
'Collegati a un dispositivo in modo che si possa calcolare l\'orario di trasmissione.';
@override
String get imageSend_longSendTitle => 'Lunga trasmissione';
@override
String imageSend_longSendBody(String duration) {
return 'Questo manterrà il canale per circa $duration.';
}
@override
String get imageSend_floodNote =>
'Instradamento delle inondazioni: ogni ripetitore nel raggio ritrasmette ogni pacchetto, quindi il canale rimane occupato più a lungo.';
@override
String get imageSend_parityTitle => 'Pacchetto di recupero';
@override
String get imageSend_paritySubtitle =>
'Un pacchetto aggiuntivo. I messaggi di gruppo non vengono riconosciuti, quindi questo permette al destinatario di ricostruire l\'immagine se un singolo pacchetto viene perso.';
@override
String get imageSend_send => 'Invia';
@override
String get imageSend_cancel => 'Annullare';
@override
String get imageSend_encodeFailed =>
'Questa immagine non può essere codificata.';
@override
String get imageSend_codecDownloading =>
'Il modello di immagine sta ancora scaricando.';
@override
String get imageSend_codecUnavailable =>
'L\'invio di immagini non è disponibile su questo dispositivo.';
@override
String get imageSend_codecDisabled =>
'I messaggi immagine sono disattivati nelle impostazioni.';
@override
String get imageSend_deviceUnsupported =>
'Questa radio non può inviare pacchetti di immagini. Collegare un dispositivo con firmware companion versione 13 o successiva.';
@override
String get imageSend_directMessagesUnsupported =>
'Le immagini viaggiano come dati di gruppo, quindi possono essere inviate solo a un canale — non tramite messaggio diretto.';
@override
String get imageSend_tooLarge =>
'Quella immagine è stata codificata in un numero di pacchetti superiore a quello consentito dal formato mesh.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'pacchetti',
one: 'pacchetto',
);
return 'Immagine inviata come $count $_temp0.';
}
@override
String imageSend_sendFailed(String error) {
return 'L\'immagine non può essere inviata: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return 'Invio immagine — pacchetto $sent di $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'Nodo $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$received di $total pacchetti';
}
@override
String get receivedImage_queued => 'In attesa di decodificare';
@override
String get receivedImage_tapToDecode => 'Tocca per decodificare';
@override
String get receivedImage_decoding => 'Ricostruire… circa 1 s';
@override
String receivedImage_incomplete(int received, int total) {
return 'Immagine incompleta — $received su $total pacchetti ricevuti';
}
@override
String get receivedImage_corrupt =>
'L\'immagine non potrebbe essere ricostruita';
@override
String get receivedImage_decoderMissing =>
'Immagine ricevuta — decodifica dell\'immagine non funzionante';
@override
String get receivedImage_evicted => 'Immagine non più memorizzata';
@override
String get receivedImage_retry => 'Prova ancora';
@override
String get receivedImage_decodeAgain => 'Decodifica di nuovo';
@override
String get receivedImage_openSettings => 'Imposta';
@override
String get receivedImage_tapToProcess => 'Premi per elaborare';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'pacchetti',
one: 'pacchetto',
);
return '$bytes byte · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds secondi';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes min $seconds s';
}
} }
+405
View File
@@ -353,6 +353,44 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'ラジオの設定が更新されました'; 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 @override
String get settings_location => '場所'; String get settings_location => '場所';
@@ -443,6 +481,13 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => '広告に場所を記載してください。'; String get settings_advertLocationSubtitle => '広告に場所を記載してください。';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS更新時にゼロホップ広告を自動送信';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'GPS位置が変化したときにゼロホップ広告を送信します(広告への位置情報の含有が必要)。';
@override @override
String get settings_multiAck => 'マルチ ACK'; String get settings_multiAck => 'マルチ ACK';
@@ -548,6 +593,12 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'チャンネル数'; String get settings_infoChannelCount => 'チャンネル数';
@override
String get settings_infoHardware => 'ハードウェア';
@override
String get settings_infoFirmware => 'ファームウェア';
@override @override
String get settings_presets => 'プリセット'; String get settings_presets => 'プリセット';
@@ -663,6 +714,18 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'ウクライナ語'; 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 @override
String get appSettings_enableMessageTracing => 'メッセージ追跡を有効にする'; String get appSettings_enableMessageTracing => 'メッセージ追跡を有効にする';
@@ -792,6 +855,9 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)'; String get appSettings_batteryLipo => 'LiPo (3.0-4.2V)';
@override
String get appSettings_batteryLipoHv => 'リポバッテリー HV(3.0〜4.35V)';
@override @override
String get appSettings_mapDisplay => '地図表示'; String get appSettings_mapDisplay => '地図表示';
@@ -845,6 +911,27 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get appSettings_lastWeek => '過去1週間'; String get appSettings_lastWeek => '過去1週間';
@override
String get appSettings_rasterTileSource => 'ラスタタイルのソース';
@override
String get appSettings_stadiaEndpoint => 'Stadia エンドポイント';
@override
String get appSettings_stadiaApiKey => 'Stadia API キー';
@override
String get appSettings_stadiaApiKeyRequired => 'Stadia Maps の利用に必要です';
@override
String appSettings_stadiaApiKeyConfigured(String maskedKey) {
return '設定済み: $maskedKey';
}
@override
String get appSettings_stadiaApiKeyDialogDescription =>
'Stadia Maps の API キーを入力してください。このアプリはラスタタイルの取得に使用します。';
@override @override
String get appSettings_offlineMapCache => 'オフライン地図キャッシュ'; String get appSettings_offlineMapCache => 'オフライン地図キャッシュ';
@@ -1259,6 +1346,20 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get channels_hashtagHint => '例:#チーム'; 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 @override
String get chat_noMessages => 'まだメッセージは届いていません'; String get chat_noMessages => 'まだメッセージは届いていません';
@@ -1314,6 +1415,15 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get chat_sendGif => 'GIF を送信'; String get chat_sendGif => 'GIF を送信';
@override
String get chat_sendImage => '画像を送信';
@override
String get chat_imagePickFailed => 'その画像を開けませんでした';
@override
String get chat_receivedGif => 'GIFを受け取りました';
@override @override
String get chat_reply => '返信'; String get chat_reply => '返信';
@@ -1946,6 +2056,42 @@ class AppLocalizationsJa extends AppLocalizations {
return '失敗したダウンロード: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2466,6 +2612,38 @@ class AppLocalizationsJa extends AppLocalizations {
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。'; 'このリピータのIDをフローパス/ループ検出タグにエンコードするために使用されるバイト数。 0=1バイト (256個のID、最大64ホップ)、1=2バイト (65,000個のID、最大32ホップ)、2=3バイト (160万個のID、最大21ホップ)。 v1.13およびそれ以前のファームウェアでは、マルチバイトパスがサポートされていません。 v1.14以降のバージョンでは、一度ネットワークが起動されると、パスが一度だけ検出されます。';
@override
String get repeater_keySettings => 'アイデンティティキーの変更';
@override
String get repeater_keySettingsSubtitle => '公開鍵/秘密鍵を変更する';
@override
String get repeater_prvKey => 'プライベートキー';
@override
String get repeater_prvKeyHelper => 'リピーター用の新しいプライベートキー。128文字の16進文字列です。';
@override
String get repeater_generatePrvKey => 'ランダムな鍵ペアを生成する';
@override
String get repeater_stopGeneratingPrvKey => 'キーペアの検索を中断';
@override
String get repeater_pubKey => '公開鍵';
@override
String get repeater_pubKeyHelper => 'これは生成された秘密鍵に対応する公開鍵です。直接設定することはできません。';
@override
String get repeater_pubKeyPrefix => '希望する接頭辞';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return 'これらの16進数で始まる公開鍵を見つけてください。予想される試行回数: $tries';
}
@override @override
String get repeater_txDelay => 'フロイド・TXでの遅延'; String get repeater_txDelay => 'フロイド・TXでの遅延';
@@ -4118,6 +4296,42 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get translation_title => '翻訳'; String get translation_title => '翻訳';
@override
String get imageMessages_enableTitle => '画像メッセージを有効にする';
@override
String get imageMessages_enableSubtitle =>
'メッシュ経由で画像を送信してください。画像モデルを一度限りのダウンロードが必要です。';
@override
String get imageMessages_modelSectionTitle => '画像モデル';
@override
String get imageMessages_downloadModel => 'ダウンロード';
@override
String get imageMessages_cancelDownload => 'キャンセル';
@override
String get imageMessages_removeModel => 'モデルを削除';
@override
String get imageMessages_modelReady => '準備完了';
@override
String get imageMessages_modelNotPublished =>
'まだ公開されていません — このビルドではダウンロードできません。';
@override
String get imageMessages_downloadFailed => '画像モデルがダウンロードできませんでした。';
@override
String get imageMessages_autoProcessTitle => '画像を自動的に処理する';
@override
String get imageMessages_autoProcessSubtitle =>
'画像が届き次第、すぐに再構築してください。1回あたり約2GBのメモリを使用します。タップで再構築を解除できます。';
@override @override
String get translation_enableTitle => '翻訳機能を有効にする'; String get translation_enableTitle => '翻訳機能を有効にする';
@@ -4393,4 +4607,195 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'パケットの追跡を解除'; String get pathMap_unfollowPacket => 'パケットの追跡を解除';
@override
String get imageSend_title => '画像を送信';
@override
String get imageSend_cropNote => '512 × 512にリサイズされました。アスペクト比は保持されませんでした。';
@override
String get imageSend_originalSize =>
'原文:\nThe early morning light filtered through the curtains, casting a soft glow on the room.\n\n翻訳:\n早朝の光がカーテンをすり抜け、部屋に柔らかな光を投げかけていた。';
@override
String get imageSend_onAirSize => '放送中';
@override
String get imageSend_quality => '品質';
@override
String get imageSend_qualityStandard => '標準';
@override
String get imageSend_qualityHigh => 'ハイ';
@override
String get imageSend_packetsLabel => 'パケット';
@override
String get imageSend_airtimeLabel => '放送時間';
@override
String get imageSend_sizeLabel => 'ペイロード';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'パケット',
one: 'パケット',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => 'ラジオの設定が不明です';
@override
String get imageSend_radioUnknownBody => '放送時間の計算ができるように、デバイスに接続してください。';
@override
String get imageSend_longSendTitle => '長距離伝送';
@override
String imageSend_longSendBody(String duration) {
return 'このチャンネルは約 $duration 間保持されます。';
}
@override
String get imageSend_floodNote =>
'洪水ルーティング:範囲内の各リピーターが各パケットを再送信するため、チャネルはこれよりも長く占有されたままになります。';
@override
String get imageSend_parityTitle => '回復パケット';
@override
String get imageSend_paritySubtitle =>
'追加のパケットが1つあります。グループメッセージは確認されないため、1つのパケットが失われても受信者が画像を再構築できます。';
@override
String get imageSend_send => '送る';
@override
String get imageSend_cancel => 'キャンセル';
@override
String get imageSend_encodeFailed => 'この画像はエンコードできませんでした。';
@override
String get imageSend_codecDownloading => '画像モデルはまだダウンロード中です。';
@override
String get imageSend_codecUnavailable => 'このデバイスでは画像送信が利用できません。';
@override
String get imageSend_codecDisabled => '設定で画像メッセージはオフになっています。';
@override
String get imageSend_deviceUnsupported =>
'このラジオは画像パケットを送信できません。コンパニオン ファームウェア 13 以降を実行しているデバイスを接続してください。';
@override
String get imageSend_directMessagesUnsupported =>
'画像はグループデータとして送信されるため、ダイレクトメッセージではなくチャンネルに送ることができます。';
@override
String get imageSend_tooLarge => 'その画像は、メッシュ形式が許容するよりも多くのパケットにエンコードされています。';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: 'パケット',
one: 'パケット',
);
return '$count $_temp0として画像を送信しました';
}
@override
String imageSend_sendFailed(String error) {
return '画像を送信できませんでした:$error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return '画像送信 — パケット $sent 件、合計 $total';
}
@override
String receivedImage_senderPrefix(String prefix) {
return 'ノード $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$total パケット中 $received パケット';
}
@override
String get receivedImage_queued => '復号を待っている';
@override
String get receivedImage_tapToDecode => 'デコードするにはタップしてください。';
@override
String get receivedImage_decoding => '再構築… 約1秒';
@override
String receivedImage_incomplete(int received, int total) {
return '画像が不完全です — $total パケット中 $received パケットが到着しました';
}
@override
String get receivedImage_corrupt => '画像を再構成することはできませんでした';
@override
String get receivedImage_decoderMissing => '画像を受信しました — 画像のデコードが失敗しました';
@override
String get receivedImage_evicted => '画像はもう保存されていません';
@override
String get receivedImage_retry => 'もう一度試してください';
@override
String get receivedImage_decodeAgain => 'もう一度デコードしてください';
@override
String get receivedImage_openSettings => '設定する';
@override
String get receivedImage_tapToProcess => '処理するにはタップしてください';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: 'パケット',
one: 'パケット',
);
return '$bytes バイト・$packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds秒';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes分 $seconds秒';
}
} }
+407 -1
View File
@@ -353,6 +353,44 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => '라디오 설정이 업데이트되었습니다.'; 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 @override
String get settings_location => '위치'; String get settings_location => '위치';
@@ -444,6 +482,14 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.'; String get settings_advertLocationSubtitle => '광고에 위치 정보를 포함하세요.';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'GPS 업데이트 시 제로 홉 광고 자동 전송';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'GPS 위치가 변경되면 제로 홉 광고를 전송합니다(광고에 위치 포함 필요).';
@override @override
String get settings_multiAck => '다중 ACK'; String get settings_multiAck => '다중 ACK';
@@ -548,6 +594,12 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get settings_infoChannelCount => '채널 수'; String get settings_infoChannelCount => '채널 수';
@override
String get settings_infoHardware => '하드웨어';
@override
String get settings_infoFirmware => '펌웨어';
@override @override
String get settings_presets => '프리셋'; String get settings_presets => '프리셋';
@@ -663,6 +715,18 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get appSettings_languageUk => '우크라이나어'; 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 @override
String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화'; String get appSettings_enableMessageTracing => '메시지 추적 기능 활성화';
@@ -794,6 +858,9 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => '리튬 폴리머 (3.0-4.2V)'; String get appSettings_batteryLipo => '리튬 폴리머 (3.0-4.2V)';
@override
String get appSettings_batteryLipoHv => '리포 HV (3.0-4.35V)';
@override @override
String get appSettings_mapDisplay => '지도 표시'; String get appSettings_mapDisplay => '지도 표시';
@@ -847,6 +914,27 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get appSettings_lastWeek => '지난 주'; 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 @override
String get appSettings_offlineMapCache => '오프라인 지도 캐시'; String get appSettings_offlineMapCache => '오프라인 지도 캐시';
@@ -1261,6 +1349,20 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get channels_hashtagHint => '예: #팀'; 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 @override
String get chat_noMessages => '아직 메시지가 없습니다.'; String get chat_noMessages => '아직 메시지가 없습니다.';
@@ -1316,6 +1418,15 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get chat_sendGif => 'GIF 보내기'; String get chat_sendGif => 'GIF 보내기';
@override
String get chat_sendImage => '이미지를 보내세요';
@override
String get chat_imagePickFailed => '그 이미지를 열 수 없었어요.';
@override
String get chat_receivedGif => 'GIF를 받았습니다';
@override @override
String get chat_reply => '답변'; String get chat_reply => '답변';
@@ -1949,6 +2060,42 @@ class AppLocalizationsKo extends AppLocalizations {
return '실패한 다운로드: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2469,7 +2616,40 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => String get repeater_pathHashModeHelper =>
'이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수: 0=1 바이트 (256개 ID, 최대 64개의 홉), 1=2 바이트 (65,000개 ID, 최대 32개의 홉), 2=3 바이트 (16백만 개 ID, 최대 21개의 홉). v1.13 및 이전 버전의 펌웨어는 다중 바이트 경로를 지원하지 않으며, 네트워크가 v1.14 이상으로 업그레이드되면 한 번만 감지합니다.'; '이 리피터의 ID를 플러드 경로/루프 감지 태그에 인코딩하는 데 사용되는 바이트 수입니다. 0=1바이트(256개 ID, 최대 64홉), 1=2바이트(65,000개 ID, 최대 32홉), 2=3바이트(1,600만 개 ID, 최대 21홉). v1.14 이전 펌웨어는 항상 1바이트 경로를 사용했으며, v1.14 이상은 2바이트 또는 3바이트 경로로 설정할 수 있습니다.';
@override
String get repeater_keySettings => '신원 키 변경';
@override
String get repeater_keySettingsSubtitle => '공개/비공개 키 쌍을 변경하세요';
@override
String get repeater_prvKey => '개인 키';
@override
String get repeater_prvKeyHelper => '리피터의 새로운 개인 키로, 128자리의 16진수 문자열입니다.';
@override
String get repeater_generatePrvKey => '임의의 키 쌍을 생성하세요.';
@override
String get repeater_stopGeneratingPrvKey => '키페어 검색 중단';
@override
String get repeater_pubKey => '공개 키';
@override
String get repeater_pubKeyHelper =>
'이것은 생성된 개인 키와 함께 사용되는 공개 키입니다. 직접 설정할 수 없습니다.';
@override
String get repeater_pubKeyPrefix => '원하는 접두사';
@override
String repeater_pubKeyPrefixHelper(int tries) {
return '이러한 16진수로 시작하는 공개 키를 찾으세요. 필요한 시도 횟수: $tries.';
}
@override @override
String get repeater_txDelay => '플러드 TX 지연'; String get repeater_txDelay => '플러드 TX 지연';
@@ -4125,6 +4305,42 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get translation_title => '번역'; 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 @override
String get translation_enableTitle => '번역 기능 활성화'; String get translation_enableTitle => '번역 기능 활성화';
@@ -4400,4 +4616,194 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => '패킷 고정 해제'; String get pathMap_unfollowPacket => '패킷 고정 해제';
@override
String get imageSend_title => '이미지를 보내세요';
@override
String get imageSend_cropNote => '512 × 512로 크기가 조정되었으며, 종횡비는 유지되지 않았습니다.';
@override
String get imageSend_originalSize => '원문 없음';
@override
String get imageSend_onAirSize => '방송 중';
@override
String get imageSend_quality => '품질';
@override
String get imageSend_qualityStandard => '표준';
@override
String get imageSend_qualityHigh => '높다';
@override
String get imageSend_packetsLabel => '패킷';
@override
String get imageSend_airtimeLabel => '방송 시간';
@override
String get imageSend_sizeLabel => '페이로드';
@override
String imageSend_packetsCount(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '패킷',
one: '패킷',
);
return '$count $_temp0';
}
@override
String imageSend_range(String min, String max) {
return '$min~$max';
}
@override
String get imageSend_unknownValue => '';
@override
String get imageSend_radioUnknownTitle => '라디오 설정 정보 없음';
@override
String get imageSend_radioUnknownBody => '방송 시간을 계산할 수 있도록 기기와 연결하세요.';
@override
String get imageSend_longSendTitle => '장거리 송신';
@override
String imageSend_longSendBody(String duration) {
return '이것은 약 $duration 동안 채널을 유지할 것입니다.';
}
@override
String get imageSend_floodNote =>
'홍수 라우팅: 범위 내의 모든 리피터가 각 패킷을 재전송하므로, 채널이 이보다 더 오래 점유 상태를 유지합니다.';
@override
String get imageSend_parityTitle => '회복 패킷';
@override
String get imageSend_paritySubtitle =>
'추가 한 개의 패킷입니다. 그룹 메시지는 확인되지 않으므로, 하나의 패킷이 손실되었을 경우 수신자가 이미지를 다시 구성할 수 있습니다.';
@override
String get imageSend_send => '보내다';
@override
String get imageSend_cancel => '취소';
@override
String get imageSend_encodeFailed => '이 이미지는 인코딩할 수 없습니다.';
@override
String get imageSend_codecDownloading => '이미지 모델이 아직 다운로드 중입니다.';
@override
String get imageSend_codecUnavailable => '이 기기에서는 이미지 전송이 불가능합니다.';
@override
String get imageSend_codecDisabled => '설정에서 이미지 메시지가 꺼져 있습니다.';
@override
String get imageSend_deviceUnsupported =>
'이 라디오는 이미지 패킷을 전송할 수 없습니다. 컴패니언 펌웨어 13 이상을 실행하는 장치를 연결해 주세요.';
@override
String get imageSend_directMessagesUnsupported =>
'이미지는 그룹 데이터로 전송되므로, 직접 메시지가 아닌 채널로만 보낼 수 있습니다.';
@override
String get imageSend_tooLarge => '그 이미지는 메시 형식이 허용하는 것보다 더 많은 패킷으로 인코딩되었습니다.';
@override
String imageSend_sentConfirmation(int count) {
String _temp0 = intl.Intl.pluralLogic(
count,
locale: localeName,
other: '패킷',
one: '패킷',
);
return '이미지가 $count $_temp0로 전송되었습니다.';
}
@override
String imageSend_sendFailed(String error) {
return '이미지를 보낼 수 없습니다: $error';
}
@override
String imageSend_sendingProgress(int sent, int total) {
return '이미지 전송 — $sent개의 $total 패킷';
}
@override
String receivedImage_senderPrefix(String prefix) {
return '노드 $prefix';
}
@override
String receivedImage_incoming(int received, int total) {
return '$total개 패킷 중 $received개';
}
@override
String get receivedImage_queued => '해독을 기다리는 중입니다.';
@override
String get receivedImage_tapToDecode => '해독하려면 탭을 누르세요.';
@override
String get receivedImage_decoding => '재구성하는… 약 1초 정도';
@override
String receivedImage_incomplete(int received, int total) {
return '이미지 불완전 — $total개$received개 패킷 수신됨';
}
@override
String get receivedImage_corrupt => '이미지는 재구성될 수 없습니다.';
@override
String get receivedImage_decoderMissing => '수신된 이미지 — 이미지 해독이 되지 않았습니다';
@override
String get receivedImage_evicted => '이미지는 더 이상 저장되지 않았습니다.';
@override
String get receivedImage_retry => '다시 시도해 보세요';
@override
String get receivedImage_decodeAgain => '다시 해독하다';
@override
String get receivedImage_openSettings => '설정하다';
@override
String get receivedImage_tapToProcess => '처리하려면 탭을 누르세요.';
@override
String receivedImage_awaiting(int bytes, int packets) {
String _temp0 = intl.Intl.pluralLogic(
packets,
locale: localeName,
other: '패킷',
one: '패킷',
);
return '$bytes 바이트 · $packets $_temp0';
}
@override
String imageSend_secondsValue(String seconds) {
return '$seconds';
}
@override
String imageSend_minutesSecondsValue(String minutes, String seconds) {
return '$minutes분 $seconds초';
}
} }
+422 -1
View File
@@ -365,6 +365,47 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Radio instellingen bijgewerkt'; 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 @override
String get settings_location => 'Locatie'; String get settings_location => 'Locatie';
@@ -462,6 +503,14 @@ class AppLocalizationsNl extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Locatie opnemen in advertentie'; '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 @override
String get settings_multiAck => 'Meerdere bevestigingen'; String get settings_multiAck => 'Meerdere bevestigingen';
@@ -571,6 +620,12 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Aantal Kanalen'; String get settings_infoChannelCount => 'Aantal Kanalen';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Voorgeprogrammeerde instellingen'; String get settings_presets => 'Voorgeprogrammeerde instellingen';
@@ -686,6 +741,18 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Oekraïens'; 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 @override
String get appSettings_enableMessageTracing => 'Berichttracking inschakelen'; String get appSettings_enableMessageTracing => 'Berichttracking inschakelen';
@@ -832,6 +899,9 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
@override @override
String get appSettings_mapDisplay => 'Kaartweergave'; String get appSettings_mapDisplay => 'Kaartweergave';
@@ -889,6 +959,28 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Afgelopen week'; 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 @override
String get appSettings_offlineMapCache => 'Offline Kaartcache'; String get appSettings_offlineMapCache => 'Offline Kaartcache';
@@ -1316,6 +1408,20 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'bijv. #team'; 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 @override
String get chat_noMessages => 'Nog geen berichten.'; String get chat_noMessages => 'Nog geen berichten.';
@@ -1371,6 +1477,15 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get chat_sendGif => 'GIF verzenden'; 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 @override
String get chat_reply => 'Reageren'; String get chat_reply => 'Reageren';
@@ -2021,6 +2136,42 @@ class AppLocalizationsNl extends AppLocalizations {
return 'Mislukte downloads: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2562,7 +2713,42 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Vertraging bij Flood TX'; String get repeater_txDelay => 'Vertraging bij Flood TX';
@@ -4325,6 +4511,43 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get translation_title => 'Vertaling'; 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 @override
String get translation_enableTitle => 'Activeer vertaling'; String get translation_enableTitle => 'Activeer vertaling';
@@ -4608,4 +4831,202 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Weergave ontgrendelen van pakket'; 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';
}
} }
+423 -1
View File
@@ -371,6 +371,47 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_radioSettingsUpdated => String get settings_radioSettingsUpdated =>
'Ustawienia radia zostały zaktualizowane'; '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 @override
String get settings_location => 'Lokalizacja'; String get settings_location => 'Lokalizacja';
@@ -469,6 +510,14 @@ class AppLocalizationsPl extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Uwzględnij lokalizację w ogłoszeniu'; '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 @override
String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru'; String get settings_multiAck => 'Wielokrotne potwierdzenia odbioru';
@@ -579,6 +628,12 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Liczba kanałów'; String get settings_infoChannelCount => 'Liczba kanałów';
@override
String get settings_infoHardware => 'Sprzęt';
@override
String get settings_infoFirmware => 'Oprogramowanie wbudowane';
@override @override
String get settings_presets => 'Presety'; String get settings_presets => 'Presety';
@@ -695,6 +750,18 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukraińska'; 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 @override
String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości'; String get appSettings_enableMessageTracing => 'Włącz śledzenie wiadomości';
@@ -842,6 +909,9 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'Bateria LiPo HV (3,0-4,35 V)';
@override @override
String get appSettings_mapDisplay => 'Wyświetlanie mapy'; String get appSettings_mapDisplay => 'Wyświetlanie mapy';
@@ -898,6 +968,28 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Ostatni tydzień'; 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 @override
String get appSettings_offlineMapCache => 'Pamięć podręczna map offline'; String get appSettings_offlineMapCache => 'Pamięć podręczna map offline';
@@ -1338,6 +1430,20 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'np. #zespół'; 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 @override
String get chat_noMessages => 'Brak jeszcze wiadomości'; String get chat_noMessages => 'Brak jeszcze wiadomości';
@@ -1394,6 +1500,15 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get chat_sendGif => 'Wyślij GIF'; 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 @override
String get chat_reply => 'Odpowiedz'; String get chat_reply => 'Odpowiedz';
@@ -2051,6 +2166,42 @@ class AppLocalizationsPl extends AppLocalizations {
return 'Nieudane pobrania: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2595,7 +2746,43 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 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 @override
String get repeater_txDelay => 'Opóźnienie w Flood, TX'; String get repeater_txDelay => 'Opóźnienie w Flood, TX';
@@ -4364,6 +4551,44 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get translation_title => 'Tłumaczenie'; 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 @override
String get translation_enableTitle => 'Włącz tłumaczenie'; String get translation_enableTitle => 'Włącz tłumaczenie';
@@ -4652,4 +4877,201 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Przestań śledzić pakiet'; 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';
}
} }
+423 -1
View File
@@ -369,6 +369,46 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_radioSettingsUpdated => String get settings_radioSettingsUpdated =>
'Configurações de rádio atualizadas'; '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 @override
String get settings_location => 'Localização'; String get settings_location => 'Localização';
@@ -466,6 +506,14 @@ class AppLocalizationsPt extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Incluir localização no anúncio'; '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 @override
String get settings_multiAck => 'Multi-ACKs'; String get settings_multiAck => 'Multi-ACKs';
@@ -577,6 +625,12 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Número do Canal'; String get settings_infoChannelCount => 'Número do Canal';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Configurações pré-definidas'; String get settings_presets => 'Configurações pré-definidas';
@@ -692,6 +746,18 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ucraniano'; 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 @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Ativar rastreamento de mensagens'; 'Ativar rastreamento de mensagens';
@@ -839,6 +905,9 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35V)';
@override @override
String get appSettings_mapDisplay => 'Exibição do Mapa'; String get appSettings_mapDisplay => 'Exibição do Mapa';
@@ -895,6 +964,28 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Da última semana'; 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 @override
String get appSettings_offlineMapCache => 'Cache de Mapa Offline'; String get appSettings_offlineMapCache => 'Cache de Mapa Offline';
@@ -1326,6 +1417,20 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'ex. #equipe'; 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 @override
String get chat_noMessages => 'Ainda não existem mensagens.'; String get chat_noMessages => 'Ainda não existem mensagens.';
@@ -1381,6 +1486,15 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get chat_sendGif => 'Enviar GIF'; 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 @override
String get chat_reply => 'Responder'; String get chat_reply => 'Responder';
@@ -2034,6 +2148,42 @@ class AppLocalizationsPt extends AppLocalizations {
return 'Downloads falhas: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2580,7 +2730,43 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Atraso na entrega em Flood, TX'; String get repeater_txDelay => 'Atraso na entrega em Flood, TX';
@@ -4342,6 +4528,44 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get translation_title => 'Tradução'; 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 @override
String get translation_enableTitle => 'Ativar a tradução'; String get translation_enableTitle => 'Ativar a tradução';
@@ -4624,4 +4848,202 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Liberar vista do pacote'; 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 '$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';
}
} }
+427 -1
View File
@@ -368,6 +368,48 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Настройки радио обновлены'; 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 @override
String get settings_location => 'Позиция'; String get settings_location => 'Позиция';
@@ -466,6 +508,14 @@ class AppLocalizationsRu extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Включить местоположение в объявление'; 'Включить местоположение в объявление';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Авто-объявление без хопов при обновлении GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).';
@override @override
String get settings_multiAck => 'Несколько подтверждений'; String get settings_multiAck => 'Несколько подтверждений';
@@ -576,6 +626,13 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Количество каналов'; String get settings_infoChannelCount => 'Количество каналов';
@override
String get settings_infoHardware => 'Оборудование';
@override
String get settings_infoFirmware =>
'Производственное программное обеспечение';
@override @override
String get settings_presets => 'Пресеты'; String get settings_presets => 'Пресеты';
@@ -692,6 +749,18 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Українська'; 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 @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Включить трассировку сообщений'; 'Включить трассировку сообщений';
@@ -841,6 +910,9 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3.04.2 В)'; String get appSettings_batteryLipo => 'LiPo (3.04.2 В)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,04,35 В)';
@override @override
String get appSettings_mapDisplay => 'Отображение карты'; String get appSettings_mapDisplay => 'Отображение карты';
@@ -898,6 +970,28 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Последнюю неделю'; 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 @override
String get appSettings_offlineMapCache => 'Кэш офлайн-карты'; String get appSettings_offlineMapCache => 'Кэш офлайн-карты';
@@ -1327,6 +1421,20 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'например, #команда'; 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 @override
String get chat_noMessages => 'Сообщений пока нет'; String get chat_noMessages => 'Сообщений пока нет';
@@ -1382,6 +1490,15 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get chat_sendGif => 'Отправить GIF'; String get chat_sendGif => 'Отправить GIF';
@override
String get chat_sendImage => 'Отправьте изображение';
@override
String get chat_imagePickFailed => 'Не удалось открыть это изображение';
@override
String get chat_receivedGif => 'Получен GIF';
@override @override
String get chat_reply => 'Ответить'; String get chat_reply => 'Ответить';
@@ -2038,6 +2155,42 @@ class AppLocalizationsRu extends AppLocalizations {
return 'Неудачных загрузок: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2585,7 +2738,42 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Задержка в работе системы Flood TX'; String get repeater_txDelay => 'Задержка в работе системы Flood TX';
@@ -4358,6 +4546,44 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get translation_title => 'Перевод'; 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 @override
String get translation_enableTitle => 'Включить перевод'; String get translation_enableTitle => 'Включить перевод';
@@ -4644,4 +4870,204 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Не следить за пакетом'; 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 с';
}
} }
+421 -1
View File
@@ -367,6 +367,45 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Nastavenia rádia aktualizované'; 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 @override
String get settings_location => 'Lokalita'; String get settings_location => 'Lokalita';
@@ -460,6 +499,14 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => 'Zahrnúť polohu do inzerátu'; 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 @override
String get settings_multiAck => 'Viaceré ACK'; String get settings_multiAck => 'Viaceré ACK';
@@ -571,6 +618,12 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Počet kanálov'; String get settings_infoChannelCount => 'Počet kanálov';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Firmware';
@override @override
String get settings_presets => 'Prednastavenia'; String get settings_presets => 'Prednastavenia';
@@ -686,6 +739,18 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukrajinská'; 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 @override
String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ'; String get appSettings_enableMessageTracing => 'Povoliť sledovanie správ';
@@ -828,6 +893,9 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3.0-4.35V)';
@override @override
String get appSettings_mapDisplay => 'Zobrazenie mapy'; String get appSettings_mapDisplay => 'Zobrazenie mapy';
@@ -885,6 +953,28 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Minul týždeň'; 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 @override
String get appSettings_offlineMapCache => 'Offline Mapa Pamäť'; String get appSettings_offlineMapCache => 'Offline Mapa Pamäť';
@@ -1316,6 +1406,20 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'napr. #tím'; 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 @override
String get chat_noMessages => 'Zatiaľ žiadne správy.'; String get chat_noMessages => 'Zatiaľ žiadne správy.';
@@ -1371,6 +1475,15 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get chat_sendGif => 'Odoslať GIF'; 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 @override
String get chat_reply => 'Odpovedať'; String get chat_reply => 'Odpovedať';
@@ -2024,6 +2137,42 @@ class AppLocalizationsSk extends AppLocalizations {
return 'Neúspešné stiahnutia: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2566,7 +2715,43 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Zpoždanie v Flood, TX'; String get repeater_txDelay => 'Zpoždanie v Flood, TX';
@@ -4323,6 +4508,43 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get translation_title => 'Preklad'; 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 @override
String get translation_enableTitle => 'Aktivovať preklad'; String get translation_enableTitle => 'Aktivovať preklad';
@@ -4608,4 +4830,202 @@ class AppLocalizationsSk extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Odomknúť pohľad od paketu'; 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';
}
} }
+422 -1
View File
@@ -366,6 +366,46 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Radio nastavitve posodobljene'; 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 @override
String get settings_location => 'Lokacija'; String get settings_location => 'Lokacija';
@@ -461,6 +501,14 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => 'Vključi lokacijo v oglas.'; 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 @override
String get settings_multiAck => 'Več potrdil'; String get settings_multiAck => 'Več potrdil';
@@ -569,6 +617,12 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Število kanalov'; String get settings_infoChannelCount => 'Število kanalov';
@override
String get settings_infoHardware => 'Hardver';
@override
String get settings_infoFirmware => 'Pravilnik';
@override @override
String get settings_presets => 'Prednastavitve'; String get settings_presets => 'Prednastavitve';
@@ -684,6 +738,18 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukrajinsko'; 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 @override
String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom'; String get appSettings_enableMessageTracing => 'Omogoči sledenje sporočilom';
@@ -830,6 +896,9 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,04,35 V)';
@override @override
String get appSettings_mapDisplay => 'Prikaz zemljevida'; String get appSettings_mapDisplay => 'Prikaz zemljevida';
@@ -886,6 +955,28 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Prejšnji teden'; 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 @override
String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave'; String get appSettings_offlineMapCache => 'Shramba zemljevidov brez povezave';
@@ -1314,6 +1405,20 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'npr. #ekipa'; 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 @override
String get chat_noMessages => 'Še ni sporočil.'; String get chat_noMessages => 'Še ni sporočil.';
@@ -1370,6 +1475,15 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get chat_sendGif => 'Pošlji GIF'; 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 @override
String get chat_reply => 'Odgovori'; String get chat_reply => 'Odgovori';
@@ -2021,6 +2135,42 @@ class AppLocalizationsSl extends AppLocalizations {
return 'Poslovniški izniki: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2562,7 +2712,43 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu'; String get repeater_txDelay => 'Zatemnitevanje zaradi poplav v Texasu';
@@ -4322,6 +4508,43 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get translation_title => 'Prevod'; 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 @override
String get translation_enableTitle => 'Omogočite prevod'; String get translation_enableTitle => 'Omogočite prevod';
@@ -4609,4 +4832,202 @@ class AppLocalizationsSl extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Odkleni pogled od paketa'; 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';
}
} }
+421 -1
View File
@@ -365,6 +365,45 @@ class AppLocalizationsSv extends AppLocalizations {
String get settings_radioSettingsUpdated => String get settings_radioSettingsUpdated =>
'Radioinställningarna har uppdaterats'; '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 @override
String get settings_location => 'Plats'; String get settings_location => 'Plats';
@@ -458,6 +497,14 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => 'Inkludera plats i annonsen'; 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 @override
String get settings_multiAck => 'Flera bekräftelser'; String get settings_multiAck => 'Flera bekräftelser';
@@ -566,6 +613,12 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Kanalantal'; String get settings_infoChannelCount => 'Kanalantal';
@override
String get settings_infoHardware => 'Hardware';
@override
String get settings_infoFirmware => 'Föredata';
@override @override
String get settings_presets => 'Fördefinierade inställningar'; String get settings_presets => 'Fördefinierade inställningar';
@@ -681,6 +734,18 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Ukrainska'; 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 @override
String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning'; String get appSettings_enableMessageTracing => 'Aktivera meddelandespårning';
@@ -823,6 +888,9 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)'; String get appSettings_batteryLipo => 'LiPo (3,0-4,2V)';
@override
String get appSettings_batteryLipoHv => 'LiPo HV (3,0-4,35 V)';
@override @override
String get appSettings_mapDisplay => 'Kartvisning'; String get appSettings_mapDisplay => 'Kartvisning';
@@ -879,6 +947,28 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Förra veckan'; 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 @override
String get appSettings_offlineMapCache => 'Offline Kartcache'; String get appSettings_offlineMapCache => 'Offline Kartcache';
@@ -1306,6 +1396,20 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get channels_hashtagHint => 't.ex. #team'; 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 @override
String get chat_noMessages => 'Inga meddelanden ännu'; String get chat_noMessages => 'Inga meddelanden ännu';
@@ -1363,6 +1467,15 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get chat_sendGif => 'Skicka GIF'; 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 @override
String get chat_reply => 'Svara'; String get chat_reply => 'Svara';
@@ -2009,6 +2122,42 @@ class AppLocalizationsSv extends AppLocalizations {
return 'Misslyckade nedladdningar: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2550,7 +2699,42 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Försening i Flood TX'; String get repeater_txDelay => 'Försening i Flood TX';
@@ -4295,6 +4479,44 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get translation_title => 'Översättning'; 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 @override
String get translation_enableTitle => 'Aktivera översättning'; String get translation_enableTitle => 'Aktivera översättning';
@@ -4579,4 +4801,202 @@ class AppLocalizationsSv extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Lås upp vy från paket'; 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';
}
} }
+424 -1
View File
@@ -367,6 +367,45 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => 'Налаштування радіо оновлено'; 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 @override
String get settings_location => 'Геопозиція'; String get settings_location => 'Геопозиція';
@@ -462,6 +501,14 @@ class AppLocalizationsUk extends AppLocalizations {
String get settings_advertLocationSubtitle => String get settings_advertLocationSubtitle =>
'Включити геопозицію в оголошення'; 'Включити геопозицію в оголошення';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate =>
'Автооголошення без хопів при оновленні GPS';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).';
@override @override
String get settings_multiAck => 'Багато підтверджень'; String get settings_multiAck => 'Багато підтверджень';
@@ -574,6 +621,12 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get settings_infoChannelCount => 'Кількість каналів'; String get settings_infoChannelCount => 'Кількість каналів';
@override
String get settings_infoHardware => 'Оборудовання';
@override
String get settings_infoFirmware => 'Програмне вміст (фirmware)';
@override @override
String get settings_presets => 'Попередні налаштування'; String get settings_presets => 'Попередні налаштування';
@@ -689,6 +742,18 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get appSettings_languageUk => 'Українська'; 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 @override
String get appSettings_enableMessageTracing => String get appSettings_enableMessageTracing =>
'Увімкнути відстеження повідомлень'; 'Увімкнути відстеження повідомлень';
@@ -835,6 +900,9 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)'; String get appSettings_batteryLipo => 'LiPo (3.0-4.2В)';
@override
String get appSettings_batteryLipoHv => 'ЛиPo HV (3,0-4,35 В)';
@override @override
String get appSettings_mapDisplay => 'Відображення карти'; String get appSettings_mapDisplay => 'Відображення карти';
@@ -892,6 +960,28 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get appSettings_lastWeek => 'Минулий тиждень'; 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 @override
String get appSettings_offlineMapCache => 'Офлайн-кеш карти'; String get appSettings_offlineMapCache => 'Офлайн-кеш карти';
@@ -1320,6 +1410,20 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get channels_hashtagHint => 'напр. #команда'; 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 @override
String get chat_noMessages => 'Поки немає повідомлень.'; String get chat_noMessages => 'Поки немає повідомлень.';
@@ -1376,6 +1480,16 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get chat_sendGif => 'Надіслати GIF'; 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 @override
String get chat_reply => 'Відповісти'; String get chat_reply => 'Відповісти';
@@ -2031,6 +2145,42 @@ class AppLocalizationsUk extends AppLocalizations {
return 'Невдалі завантаження: $count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2582,7 +2732,42 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => 'Затримка у Flood, штат Техас'; String get repeater_txDelay => 'Затримка у Flood, штат Техас';
@@ -4355,6 +4540,43 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get translation_title => 'Переклад'; 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 @override
String get translation_enableTitle => 'Увімкнути переклад'; String get translation_enableTitle => 'Увімкнути переклад';
@@ -4644,4 +4866,205 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => 'Відв\'язати вигляд від пакету'; 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 с';
}
} }
+402 -1
View File
@@ -350,6 +350,44 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_radioSettingsUpdated => '无线电设置已更新'; 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 @override
String get settings_location => '位置'; String get settings_location => '位置';
@@ -437,6 +475,13 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_advertLocationSubtitle => '在广告中包含位置'; String get settings_advertLocationSubtitle => '在广告中包含位置';
@override
String get settings_autoZeroHopAdvertOnGpsUpdate => 'GPS 更新时自动发送零跳广告';
@override
String get settings_autoZeroHopAdvertOnGpsUpdateSubtitle =>
'当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。';
@override @override
String get settings_multiAck => '多重ACK'; String get settings_multiAck => '多重ACK';
@@ -541,6 +586,12 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get settings_infoChannelCount => '频道数量'; String get settings_infoChannelCount => '频道数量';
@override
String get settings_infoHardware => '硬件';
@override
String get settings_infoFirmware => '固件';
@override @override
String get settings_presets => '预设'; String get settings_presets => '预设';
@@ -655,6 +706,18 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get appSettings_languageUk => '乌克兰语'; 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 @override
String get appSettings_enableMessageTracing => '启用消息追踪'; String get appSettings_enableMessageTracing => '启用消息追踪';
@@ -779,6 +842,9 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)'; String get appSettings_batteryLipo => '锂聚合物电池 (3.0-4.2V)';
@override
String get appSettings_batteryLipoHv => '锂多压电源(3.0-4.35伏)';
@override @override
String get appSettings_mapDisplay => '地图显示'; String get appSettings_mapDisplay => '地图显示';
@@ -832,6 +898,27 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get appSettings_lastWeek => '上周'; 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 @override
String get appSettings_offlineMapCache => '离线地图缓存'; String get appSettings_offlineMapCache => '离线地图缓存';
@@ -1245,6 +1332,20 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get channels_hashtagHint => '例如:#团队'; 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 @override
String get chat_noMessages => '暂无消息'; String get chat_noMessages => '暂无消息';
@@ -1300,6 +1401,15 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get chat_sendGif => '发送 GIF'; String get chat_sendGif => '发送 GIF';
@override
String get chat_sendImage => '发送图片';
@override
String get chat_imagePickFailed => '无法打开该图片';
@override
String get chat_receivedGif => '收到一个GIF';
@override @override
String get chat_reply => '回复'; String get chat_reply => '回复';
@@ -1925,6 +2035,42 @@ class AppLocalizationsZh extends AppLocalizations {
return '下载失败:$count'; 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 @override
String mapCache_boundsLabel( String mapCache_boundsLabel(
String north, String north,
@@ -2439,7 +2585,39 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get repeater_pathHashModeHelper => 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 @override
String get repeater_txDelay => '洪水(德克萨斯州)延误'; String get repeater_txDelay => '洪水(德克萨斯州)延误';
@@ -4014,6 +4192,40 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get translation_title => '翻译'; 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 @override
String get translation_enableTitle => '启用翻译功能'; String get translation_enableTitle => '启用翻译功能';
@@ -4288,4 +4500,193 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get pathMap_unfollowPacket => '解锁视图跟随'; 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';
}
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.", "settings_locationInvalid": "Ongeldige breedtegraad of lengtegraad.",
"settings_latitude": "Breedtegraad", "settings_latitude": "Breedtegraad",
"settings_longitude": "Lengtegraad", "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_privacyMode": "Privacy-modus",
"settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties", "settings_privacyModeSubtitle": "Naam/locatie verbergen in advertenties",
"settings_privacyModeToggle": "Schakel privacy modus in om je naam en locatie in advertenties te verbergen.", "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_last6Hours": "Afgelopen 6 uur",
"appSettings_last24Hours": "Afgelopen 24 uur", "appSettings_last24Hours": "Afgelopen 24 uur",
"appSettings_lastWeek": "Afgelopen week", "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_offlineMapCache": "Offline Kaartcache",
"appSettings_noAreaSelected": "Geen gebied geselecteerd", "appSettings_noAreaSelected": "Geen gebied geselecteerd",
"appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})", "appSettings_areaSelectedZoom": "Geselecteerd gebied (zoom {minZoom}-{maxZoom})",
@@ -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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1107,7 +1172,7 @@
"repeater_advancedSettings": "Geavanceerd", "repeater_advancedSettings": "Geavanceerd",
"repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers", "repeater_advancedSettingsSubtitle": "Regelhendels voor ervaren gebruikers",
"repeater_pathHashMode": "Hash-modus voor paden", "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_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_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", "repeater_directTxDelay": "Directe vertraging",
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Geen locatie", "pathMap_noLocation": "Geen locatie",
"pathMap_followPacket": "Weergave vergrendelen op pakket", "pathMap_followPacket": "Weergave vergrendelen op pakket",
"pathMap_unfollowPacket": "Weergave ontgrendelen van pakket", "pathMap_unfollowPacket": "Weergave ontgrendelen van pakket",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regio's",
"settings_regionSettingsSubtitle": "Beheer van opgeslagen regio's",
"settings_regionManagement_screenTitle": "Regionale Bestuur",
"settings_regionNameHint": "Regio naam invoeren",
"settings_regionAddRegion": "Regio toevoegen",
"settings_regionFetchRegions": "Haal regio's uit herhalers",
"settings_regionFetchRegionsFail": "Er zijn geen regio's gevonden",
"settings_regionFetchRegionsAlreadyExists": "Deze regio is al toegevoegd.",
"settings_regionName": "Regio Naam",
"settings_regionDeleted": "Regio verwijderd",
"settings_deleteRegion": "Regio verwijden",
"settings_deleteRegionConfirm": "Verwijder \"{region}\" uit de regio-lijst?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Regio: {region}",
"channels_regionNotSet": "Regio: geen",
"channels_regionSelect_Title": "Kies een regio",
"channels_clearRegion": "Duidelijke regio",
"chat_sendImage": "Beeld verzenden",
"chat_imagePickFailed": "Kunde die afbeelding niet openen",
"chat_receivedGif": "Ontvangen van een GIF",
"repeater_keySettings": "Verander de Identiteitskies",
"repeater_keySettingsSubtitle": "Wijzig de openbare/private sleutelpaar",
"repeater_prvKey": "Geheime sleutel",
"repeater_prvKeyHelper": "Een nieuwe private sleutel voor de repeater, een 128-karakter lange hexadecimale string.",
"repeater_generatePrvKey": "Stel een willekeurige sleutelpaar op",
"repeater_stopGeneratingPrvKey": "Stop zoek naar sleutelpaar",
"repeater_pubKey": "Openbaar sleutel",
"repeater_pubKeyHelper": "Dit is de openbare sleutel die past bij de gegenereerde privésleutel. U kunt dit niet direct instellen.",
"repeater_pubKeyPrefix": "Verlangde voorvoegsel",
"repeater_pubKeyPrefixHelper": "Zoek een openbare sleutel die begint met deze hexadecimale cijfers. Verwacht aantal pogingen: {tries}.",
"imageMessages_enableTitle": "Bijbehorende afbeeldingen activeren",
"imageMessages_enableSubtitle": "Beeldjes verzenden via het netwerk. Vereist eenmalig afdaling van een beeldmodel.",
"imageMessages_modelSectionTitle": "Beeldmodel",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Annuleren",
"imageMessages_removeModel": "Model verwijderen",
"imageMessages_modelReady": "Klaar",
"imageMessages_modelNotPublished": "Nog niet gepubliceerd — deze versie kan dit niet downloaden.",
"imageMessages_downloadFailed": "Het beeldmodel kon niet worden gedownload.",
"imageMessages_autoProcessTitle": "Beeldverwerking automatisch",
"imageMessages_autoProcessSubtitle": "Reconstrueer elk beeld direct nadat het binnenkomt. Het gebruikt ongeveer 2 GB geheugen voor elke keer; schakel het reconstrueren uit met een tik als je het niet nodig hebt.",
"imageSend_title": "Beeld verzenden",
"imageSend_cropNote": "Aangepast aan 512 × 512 · het aspectverhouding is niet behouden",
"imageSend_originalSize": "Oorspronkelijk",
"imageSend_onAirSize": "Op de lucht",
"imageSend_quality": "Kwaliteit",
"imageSend_qualityStandard": "Standaard",
"imageSend_qualityHigh": "Hoog",
"imageSend_packetsLabel": "Pakketten",
"imageSend_airtimeLabel": "Tijd op de lucht",
"imageSend_sizeLabel": "Belasting",
"imageSend_packetsCount": "{count} {count, plural, =1{pakket} other{pakketten}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radio-instellingen onbekend",
"imageSend_radioUnknownBody": "Verbinding maken met een apparaat zodat de uitzendtijd kan worden berekend.",
"imageSend_longSendTitle": "Lange transmissie",
"imageSend_longSendBody": "Dit houdt de kanaal ongeveer voor {duration} stand.",
"imageSend_floodNote": "Overstromingsrouting: elke repeater binnen bereik herzendsends elk pakket opnieuw, waardoor het kanaal langer bezig blijft dan dit.",
"imageSend_parityTitle": "Herstelpakket",
"imageSend_paritySubtitle": "Een extra pakje. Groepsberichten worden niet bevestigd, waardoor de ontvanger het beeld kan herstellen als een enkel pakket verloren gaat.",
"imageSend_send": "Stuur",
"imageSend_cancel": "Annuleren",
"imageSend_encodeFailed": "Dit beeld kon niet worden gecodeerd.",
"imageSend_codecDownloading": "Het beeldmodel wordt nog steeds gedownload.",
"imageSend_codecUnavailable": "Het verzenden van een afbeelding is niet mogelijk op dit apparaat.",
"imageSend_codecDisabled": "Boodschappen met afbeeldingen zijn uitgeschakeld in de instellingen.",
"imageSend_deviceUnsupported": "Dit radio kan geen beeldpakketten verzenden. Verbind een apparaat met de bijbehorende firmware versie 13 of nieuwer.",
"imageSend_directMessagesUnsupported": "Afbeeldingen worden verzonden als groepsgegevens, dus ze kunnen alleen naar een kanaal worden gestuurd — niet via een directe boodschap.",
"imageSend_tooLarge": "Die afbeelding werd gecodeerd in meer pakketten dan het mesh-formaat toelaat.",
"imageSend_sentConfirmation": "Afbeelding verzonden als {count} {count, plural, =1{pakket} other{pakketten}}.",
"imageSend_sendFailed": "Het beeld kon niet worden verzonden: {error}",
"imageSend_sendingProgress": "Beeld verzenden — pakket {sent} van {total}",
"receivedImage_senderPrefix": "Knipper {prefix}",
"receivedImage_incoming": "{received} van de {total} pakketten",
"receivedImage_queued": "Wachten op het ontcijferen",
"receivedImage_tapToDecode": "Tik om te decoderen",
"receivedImage_decoding": "Herstellen… ongeveer 1 seconde",
"receivedImage_incomplete": "Afbeelding onvolledig — {received} van {total} pakketten ontvangen",
"receivedImage_corrupt": "Afbeelding kon niet worden gereconstrueerd",
"receivedImage_decoderMissing": "Beeld ontvangen — beeldontcipheringsapparatuur is uitgeschakeld",
"receivedImage_evicted": "Afbeelding niet langer opgeslagen",
"receivedImage_retry": "Probeer het opnieuw",
"receivedImage_decodeAgain": "Decode opnieuw",
"receivedImage_openSettings": "Inricht",
"receivedImage_tapToProcess": "Tik om te verwerken",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pakket} other{pakketten}}",
"imageSend_secondsValue": "{seconds} seconden",
"imageSend_minutesSecondsValue": "{minutes} m {seconds} s"
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.", "settings_locationInvalid": "Nieprawidłowa szerokość geograficzna lub długość geograficzna.",
"settings_latitude": "Szerokość", "settings_latitude": "Szerokość",
"settings_longitude": "Długość", "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_privacyMode": "Tryb prywatności",
"settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach", "settings_privacyModeSubtitle": "Ukryj imię/lokalizację w rozgłoszeniach",
"settings_privacyModeToggle": "Włącz tryb prywatności, aby ukryć swoje imię i 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_last6Hours": "Ostatnie 6 godzin",
"appSettings_last24Hours": "Ostatnie 24 godziny", "appSettings_last24Hours": "Ostatnie 24 godziny",
"appSettings_lastWeek": "Ostatni tydzień", "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_offlineMapCache": "Pamięć podręczna map offline",
"appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.", "appSettings_noAreaSelected": "Nie wybrano żadnego obszaru.",
"appSettings_areaSelectedZoom": "Wybrany obszar (skala {minZoom}-{maxZoom})", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1117,7 +1182,7 @@
"repeater_advancedSettings": "Zaawansowany", "repeater_advancedSettings": "Zaawansowany",
"repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów", "repeater_advancedSettingsSubtitle": "Regulowane pokrętła dla doświadczonych operatorów",
"repeater_pathHashMode": "Tryb haszujący ścieżkę", "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 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_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_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", "repeater_directTxDelay": "Bezpośrednie opóźnienie sygnału TX",
@@ -2578,5 +2643,227 @@
"pathMap_noLocation": "Brak lokalizacji", "pathMap_noLocation": "Brak lokalizacji",
"pathMap_followPacket": "Śledź pakiet", "pathMap_followPacket": "Śledź pakiet",
"pathMap_unfollowPacket": "Przestań śledzić pakiet", "pathMap_unfollowPacket": "Przestań śledzić pakiet",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiony",
"settings_regionSettingsSubtitle": "Zarządzanie zapisanymi regionami",
"settings_regionManagement_screenTitle": "Zarządzanie Regionami",
"settings_regionNameHint": "Wprowadź nazwę regionu",
"settings_regionAddRegion": "Dodaj region",
"settings_regionFetchRegions": "Pobierz regiony z repeaterów",
"settings_regionFetchRegionsFail": "Nie znaleziono żadnych regionów",
"settings_regionFetchRegionsAlreadyExists": "Ten region został już dodany",
"settings_regionName": "Nazwa Regionu",
"settings_regionDeleted": "Region usunięto",
"settings_deleteRegion": "Usuń Region",
"settings_deleteRegionConfirm": "Czy usunąć \"{region}\" z listy regionów?",
"settings_infoHardware": "Sprzęt",
"settings_infoFirmware": "Oprogramowanie wbudowane",
"repeater_pathHashModeOption0": "0 - 1 bajt",
"repeater_pathHashModeOption1": "1 - 2 bity",
"repeater_pathHashModeOption2": "2 - 3 bajty",
"repeater_pathHashModeOption3": "3 - 4 bajty",
"appSettings_batteryLipoHv": "Bateria LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: brak",
"channels_regionSelect_Title": "Wybierz region",
"channels_clearRegion": "Przejrzyste obszar",
"chat_sendImage": "Wyślij obraz",
"chat_imagePickFailed": "Nie mogłem otworzyć tego obrazu",
"chat_receivedGif": "Otrzymano GIF",
"repeater_keySettings": "Zmiana kluczy tożsamości",
"repeater_keySettingsSubtitle": "Zmień parę kluczy publiczną/prywatną",
"repeater_prvKey": "Klucz prywatny",
"repeater_prvKeyHelper": "Nowy klucz prywatny dla wzmacniacza, 128-znakowa string hexadecyimalny.",
"repeater_generatePrvKey": "Wygeneruj losowy klucz parowy",
"repeater_stopGeneratingPrvKey": "Przerwij wyszukiwanie kluczowego paru",
"repeater_pubKey": "Klucz publiczny",
"repeater_pubKeyHelper": "To jest klucz publiczny, który odpowiada wygenerowanemu kluczowi prywatnemu. Nie można go ustawić bezpośrednio.",
"repeater_pubKeyPrefix": "Pożądane przedrostek",
"repeater_pubKeyPrefixHelper": "Znajdź klucz publiczny, który zaczyna się od tych cyfr hex. Oczekiwana liczba prób: {tries}.",
"imageMessages_enableTitle": "Włącz wiadomości ze zdjęciami",
"imageMessages_enableSubtitle": "Przesyłaj zdjęcia przez siatkę. Wymaga jednorazowego pobrania modelu obrazu.",
"imageMessages_modelSectionTitle": "Model obrazowy",
"imageMessages_downloadModel": "Pobierz",
"imageMessages_cancelDownload": "Anuluj",
"imageMessages_removeModel": "Usuń model",
"imageMessages_modelReady": "Gotowy",
"imageMessages_modelNotPublished": "Nie opublikowane — ten build nie może tego pobrać.",
"imageMessages_downloadFailed": "Model obrazowy nie mógł zostać pobrany.",
"imageMessages_autoProcessTitle": "Obrazki są przetwarzane automatycznie.",
"imageMessages_autoProcessSubtitle": "Odbuduj każde zdjęcie, jak tylko przyjdzie. Zużywa około 2 GB pamięci za każdą drugą sekundę; wyłącz odbudowywanie jednym dotknięciem.",
"imageSend_title": "Wyślij obraz",
"imageSend_cropNote": "Zmniejszone do 512 × 512 · proporcje nie zostały zachowane",
"imageSend_originalSize": "Oryginalny",
"imageSend_onAirSize": "W eterze",
"imageSend_quality": "Jakość",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Wysoko",
"imageSend_packetsLabel": "Pakiet",
"imageSend_airtimeLabel": "Czas w eterze",
"imageSend_sizeLabel": "Payload",
"imageSend_packetsCount": "{count} {count, plural, =1{pakiet} other{pakietów}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Ustawienia radia nieznane",
"imageSend_radioUnknownBody": "Połącz urządzenie, aby można było obliczyć czas trwania transmisji.",
"imageSend_longSendTitle": "Długa transmisja",
"imageSend_longSendBody": "Ten kanał będzie dostępny przez około {duration}.",
"imageSend_floodNote": "Przepływ pakietów: każdy retransmitujący w zasięgu przekazuje każdy pakiet, przez co kanał pozostaje zajęty dłużej niż ten czas.",
"imageSend_parityTitle": "Pakiet odzysku",
"imageSend_paritySubtitle": "Jedna dodatkowa paczka. Wiadomości grupowe nie są potwierdzane, dzięki czemu odbiorca może odbudować obraz, jeśli zostanie utracona pojedyncza paczka.",
"imageSend_send": "Wyślij",
"imageSend_cancel": "Anuluj",
"imageSend_encodeFailed": "Ten obraz nie mógł zostać zakodowany.",
"imageSend_codecDownloading": "Model obrazowy jest nadal pobierany.",
"imageSend_codecUnavailable": "Wysyłanie obrazu nie jest dostępne na tym urządzeniu.",
"imageSend_codecDisabled": "Wiadomości ze zdjęciami są wyłączone w ustawieniach.",
"imageSend_deviceUnsupported": "Ten radiodysk nie może wysyłać pakietów obrazu. Podłącz urządzenie z oprogramowaniem firmware 13 lub nowszym.",
"imageSend_directMessagesUnsupported": "Obrazy są przesyłane jako dane grupowe, więc można je wysłać tylko do kanału — nie w wiadomości prywatnej.",
"imageSend_tooLarge": "Obraz zakodowany do więcej pakietów niż pozwala format siatki.",
"imageSend_sentConfirmation": "Obraz wysłany jako {count} {count, plural, =1{pakiet} other{pakietów}}.",
"imageSend_sendFailed": "Obraz nie mogł zostać wysłany: {error}",
"imageSend_sendingProgress": "Wysyłanie obrazu — pakiet {sent} z {total}",
"receivedImage_senderPrefix": "Węzeł {prefix}",
"receivedImage_incoming": "Otrzymano: {received} z {total} pakietów",
"receivedImage_queued": "Oczekiwanie na rozszyfrowanie",
"receivedImage_tapToDecode": "Kliknij, aby odszyfrować",
"receivedImage_decoding": "Odtwarzanie… około 1 sekundy",
"receivedImage_incomplete": "Obraz niepełny — {received} z {total} pakietów dotarło",
"receivedImage_corrupt": "Obraz nie może zostać zrekonstruowany",
"receivedImage_decoderMissing": "Otrzymano obraz — dekodowanie obrazu jest nieprawidłowe.",
"receivedImage_evicted": "Obraz nie jest już przechowywany",
"receivedImage_retry": "Spróbuj ponownie",
"receivedImage_decodeAgain": "Zdekoduj ponownie",
"receivedImage_openSettings": "Ustaw",
"receivedImage_tapToProcess": "Kliknij, aby przetworzyć",
"receivedImage_awaiting": "{bytes} bajtów · {packets} {packets, plural, =1{pakiet} other{pakiety}}",
"imageSend_secondsValue": "{seconds} sekund",
"imageSend_minutesSecondsValue": "{minutes} minut {seconds} sekundy"
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Latitude ou longitude inválidos.", "settings_locationInvalid": "Latitude ou longitude inválidos.",
"settings_latitude": "Latitude", "settings_latitude": "Latitude",
"settings_longitude": "Longitude", "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_privacyMode": "Modo de Privacidade",
"settings_privacyModeSubtitle": "Esconder nome/localização em anúncios", "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.", "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_last6Hours": "Últimos 6 horas",
"appSettings_last24Hours": "Últimas 24 horas", "appSettings_last24Hours": "Últimas 24 horas",
"appSettings_lastWeek": "Da última semana", "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_offlineMapCache": "Cache de Mapa Offline",
"appSettings_noAreaSelected": "Nenhuma área selecionada", "appSettings_noAreaSelected": "Nenhuma área selecionada",
"appSettings_areaSelectedZoom": "Área selecionada (zoom {minZoom}-{maxZoom})", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1107,7 +1172,7 @@
"repeater_advancedSettings": "Avançado", "repeater_advancedSettings": "Avançado",
"repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes", "repeater_advancedSettingsSubtitle": "Controles de ajuste para operadores experientes",
"repeater_pathHashMode": "Modo de hash de caminho", "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_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_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", "repeater_directTxDelay": "Atraso direto no sinal TX",
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Sem localização", "pathMap_noLocation": "Sem localização",
"pathMap_followPacket": "Fixar vista no pacote", "pathMap_followPacket": "Fixar vista no pacote",
"pathMap_unfollowPacket": "Liberar vista do pacote", "pathMap_unfollowPacket": "Liberar vista do pacote",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiões",
"settings_regionSettingsSubtitle": "Gerenciar regiões armazenadas",
"settings_regionManagement_screenTitle": "Gestão de Região",
"settings_regionNameHint": "Nome da região",
"settings_regionAddRegion": "Adicionar região",
"settings_regionFetchRegions": "Regiões de obtenção a partir de repetidores",
"settings_regionFetchRegionsFail": "Nenhuma região foi encontrada",
"settings_regionFetchRegionsAlreadyExists": "Esta região já foi adicionada",
"settings_regionName": "Nome da Região",
"settings_regionDeleted": "Região excluída",
"settings_deleteRegion": "Excluir Região",
"settings_deleteRegionConfirm": "Remover \"{region}\" da lista de regiões?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 bytes",
"repeater_pathHashModeOption2": "2 - 3 bytes",
"repeater_pathHashModeOption3": "3 - 4 bytes",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35V)",
"channels_regionSetTo": "Região: {region}",
"channels_regionNotSet": "Região: nenhuma",
"channels_regionSelect_Title": "Escolha uma região",
"channels_clearRegion": "Região desimpedida",
"chat_sendImage": "Enviar imagem",
"chat_imagePickFailed": "Não consegui abrir essa imagem",
"chat_receivedGif": "Recebi um GIF",
"repeater_keySettings": "Alterar Chaves de Identidade",
"repeater_keySettingsSubtitle": "Altere a chave pública/chave privada",
"repeater_prvKey": "Chave privada",
"repeater_prvKeyHelper": "Uma nova chave privada para o repetidor, uma string hexadecimal de 128 caracteres.",
"repeater_generatePrvKey": "Gere um par de chaves aleatório",
"repeater_stopGeneratingPrvKey": "Interrompa a busca por par de chaves",
"repeater_pubKey": "Chave pública",
"repeater_pubKeyHelper": "Esta é a chave pública que acompanha a chave privada gerada. Você não pode definir isso diretamente.",
"repeater_pubKeyPrefix": "Prefixo desejado",
"repeater_pubKeyPrefixHelper": "Encontre uma chave pública que comece com esses dígitos hexadecimais. Número esperado de tentativas: {tries}.",
"imageMessages_enableTitle": "Mensagens de imagem",
"imageMessages_enableSubtitle": "Envie as imagens pela malha. Requer o download de um modelo de imagem único.",
"imageMessages_modelSectionTitle": "Modelo de imagem",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Cancelar",
"imageMessages_removeModel": "Remover modelo",
"imageMessages_modelReady": "Pronto",
"imageMessages_modelNotPublished": "Ainda não publicado — esta versão não pode baixá-lo.",
"imageMessages_downloadFailed": "O modelo de imagem não pôde ser baixado.",
"imageMessages_autoProcessTitle": "Processar imagens automaticamente",
"imageMessages_autoProcessSubtitle": "Reconstrua cada imagem assim que ela chegar. Utiliza cerca de 2 GB de memória por segundo; dispense a reconstrução com um toque.",
"imageSend_title": "Enviar imagem",
"imageSend_cropNote": "Redimensionado para 512 × 512 · a proporção de aspecto não foi preservada",
"imageSend_originalSize": "Texto Original",
"imageSend_onAirSize": "Ao ar",
"imageSend_quality": "Qualidade",
"imageSend_qualityStandard": "Padrão",
"imageSend_qualityHigh": "Alto",
"imageSend_packetsLabel": "Pacotes",
"imageSend_airtimeLabel": "Tempo em transmissão",
"imageSend_sizeLabel": "Carga útil",
"imageSend_packetsCount": "{count} {count, plural, =1{pacote} other{pacotes}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Configurações de rádio desconhecidas",
"imageSend_radioUnknownBody": "Conecte-se a um dispositivo para que o horário de transmissão possa ser calculado.",
"imageSend_longSendTitle": "Transmissão longa",
"imageSend_longSendBody": "Isso manterá o canal por cerca de {duration}.",
"imageSend_floodNote": "Roteamento de inundação: cada repetidor na faixa retransmite cada pacote, fazendo com que o canal permaneça ocupado por mais tempo do que isso.",
"imageSend_parityTitle": "Pacote de recuperação",
"imageSend_paritySubtitle": "Um pacote extra. As mensagens em grupo não são reconhecidas, assim permitindo que o destinatário reconstrua a imagem caso um pacote seja perdido.",
"imageSend_send": "Enviar",
"imageSend_cancel": "Cancelar",
"imageSend_encodeFailed": "Esta imagem não pode ser codificada.",
"imageSend_codecDownloading": "O modelo de imagem ainda está baixando.",
"imageSend_codecUnavailable": "O envio de imagem não está disponível neste dispositivo.",
"imageSend_codecDisabled": "As mensagens de imagem estão desativadas nas configurações.",
"imageSend_deviceUnsupported": "Este rádio não pode enviar pacotes de imagem. Conecte um dispositivo com firmware companheiro versão 13 ou superior.",
"imageSend_directMessagesUnsupported": "As imagens viajam como dados em grupo, portanto, só podem ser enviadas para um canal — não em mensagem direta.",
"imageSend_tooLarge": "Essa imagem foi codificada em mais pacotes do que o formato de malha permite.",
"imageSend_sentConfirmation": "Imagem enviada como {count} {count, plural, =1{pacote} other{pacotes}}.",
"imageSend_sendFailed": "A imagem não pôde ser enviada: {error}",
"imageSend_sendingProgress": "Envio da imagem — pacote {sent} de {total}",
"receivedImage_senderPrefix": "Nó {prefix}",
"receivedImage_incoming": "{received} de {total} pacotes",
"receivedImage_queued": "Aguardando para decodificar",
"receivedImage_tapToDecode": "Toque para decodificar",
"receivedImage_decoding": "Reconstruindo… cerca de 1 segundo",
"receivedImage_incomplete": "Imagem incompleta — {received} de {total} pacotes chegaram",
"receivedImage_corrupt": "A imagem não pôde ser reconstruída",
"receivedImage_decoderMissing": "Imagem recebida — decodificação de imagem incorreta",
"receivedImage_evicted": "Imagem não armazenada",
"receivedImage_retry": "Tente novamente",
"receivedImage_decodeAgain": "Decodifique novamente",
"receivedImage_openSettings": "Configurar",
"receivedImage_tapToProcess": "Toque para processar",
"receivedImage_awaiting": "{bytes} bytes · {packets} {packets, plural, =1{pacote} other{pacotes}}",
"imageSend_secondsValue": "{seconds} segundos",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+289 -2
View File
@@ -77,6 +77,8 @@
"settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.", "settings_locationIntervalInvalid": "Интервал должен составлять не менее 60 секунд и не более 86400 секунд.",
"settings_latitude": "Широта", "settings_latitude": "Широта",
"settings_longitude": "Долгота", "settings_longitude": "Долгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Авто-объявление без хопов при обновлении GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Когда GPS-местоположение меняется, отправлять объявление без хопов (требуется геопозиция в объявлении).",
"settings_privacyMode": "Режим конфиденциальности", "settings_privacyMode": "Режим конфиденциальности",
"settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании", "settings_privacyModeSubtitle": "Скрыть имя/позицию в анонсировании",
"settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.", "settings_privacyModeToggle": "Включите режим конфиденциальности, чтобы скрыть свое имя и местоположение в анонсировании.",
@@ -190,6 +192,19 @@
"appSettings_last6Hours": "Последние 6 часов", "appSettings_last6Hours": "Последние 6 часов",
"appSettings_last24Hours": "Последние 24 часа", "appSettings_last24Hours": "Последние 24 часа",
"appSettings_lastWeek": "Последнюю неделю", "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_offlineMapCache": "Кэш офлайн-карты",
"appSettings_noAreaSelected": "Область не выбрана", "appSettings_noAreaSelected": "Область не выбрана",
"appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})", "appSettings_areaSelectedZoom": "Область выбрана (масштаб {minZoom}{maxZoom})",
@@ -438,6 +453,56 @@
"mapCache_downloadTilesButton": "Загрузить плитки", "mapCache_downloadTilesButton": "Загрузить плитки",
"mapCache_clearCacheButton": "Очистить кэш", "mapCache_clearCacheButton": "Очистить кэш",
"mapCache_failedDownloads": "Неудачных загрузок: {count}", "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}", "mapCache_boundsLabel": "С {north}, Ю {south}, В {east}, З {west}",
"time_justNow": "Только что", "time_justNow": "Только что",
"time_minutesAgo": "{minutes} мин назад", "time_minutesAgo": "{minutes} мин назад",
@@ -1478,7 +1543,7 @@
"repeater_advancedSettings": "Продвинутый", "repeater_advancedSettings": "Продвинутый",
"repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов", "repeater_advancedSettingsSubtitle": "Регуляторы для опытных операторов",
"repeater_pathHashMode": "Режим хеширования пути", "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_txDelay": "Задержка в работе системы Flood TX",
"repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.", "repeater_txDelayHelper": "Передача с увеличенным интервалом для трафика во время наводнения, в качестве коэффициента, умножающего время передачи пакета (от 0 до 2, по умолчанию 0,5). Более высокое значение означает меньшее количество столкновений, но более медленную передачу.",
"repeater_directTxDelay": "Прямая задержка сигнала TX", "repeater_directTxDelay": "Прямая задержка сигнала TX",
@@ -1843,5 +1908,227 @@
"pathMap_noLocation": "Нет координат", "pathMap_noLocation": "Нет координат",
"pathMap_followPacket": "Следить за пакетом", "pathMap_followPacket": "Следить за пакетом",
"pathMap_unfollowPacket": "Не следить за пакетом", "pathMap_unfollowPacket": "Не следить за пакетом",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Регионы",
"settings_regionSettingsSubtitle": "Управление сохранёнными регионами",
"settings_regionManagement_screenTitle": "Управление регионами",
"settings_regionNameHint": "Введите название региона",
"settings_regionAddRegion": "Добавить регион",
"settings_regionFetchRegions": "Извлекать регионы с ретрансляторов",
"settings_regionFetchRegionsFail": "Не были найдены никакие регионы",
"settings_regionFetchRegionsAlreadyExists": "Этот регион уже был добавлен",
"settings_regionName": "Название региона",
"settings_regionDeleted": "Регион удалён",
"settings_deleteRegion": "Удалить регион",
"settings_deleteRegionConfirm": "Удалить \"{region}\" из списка регионов?",
"settings_infoHardware": "Оборудование",
"settings_infoFirmware": "Производственное программное обеспечение",
"repeater_pathHashModeOption0": "0 - 1 байт",
"repeater_pathHashModeOption1": "1 - 2 байта",
"repeater_pathHashModeOption2": "2 - 3 байта",
"repeater_pathHashModeOption3": "3 - 4 байта",
"appSettings_batteryLipoHv": "LiPo HV (3,04,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} с"
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Neplatná šírka alebo dĺžka.", "settings_locationInvalid": "Neplatná šírka alebo dĺžka.",
"settings_latitude": "Súradnica", "settings_latitude": "Súradnica",
"settings_longitude": "Dĺžka", "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_privacyMode": "Režim ochrany súkromia",
"settings_privacyModeSubtitle": "Skryť meno/poloha v reklamách", "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.", "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_last6Hours": "Posledné 6 hodín",
"appSettings_last24Hours": "Posledných 24 hodín", "appSettings_last24Hours": "Posledných 24 hodín",
"appSettings_lastWeek": "Minul týždeň", "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_offlineMapCache": "Offline Mapa Pamäť",
"appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť", "appSettings_noAreaSelected": "Neoznačila sa žiadna oblasť",
"appSettings_areaSelectedZoom": "Vyberená oblasť (zoom {minZoom}-{maxZoom})", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1107,7 +1172,7 @@
"repeater_advancedSettings": "Pokročilé", "repeater_advancedSettings": "Pokročilé",
"repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov", "repeater_advancedSettingsSubtitle": "Ovládacie knopy pre skúsených operátorov",
"repeater_pathHashMode": "Režim hashovania cesty", "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_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_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", "repeater_directTxDelay": "Priame oneskorenie TX",
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Bez polohy", "pathMap_noLocation": "Bez polohy",
"pathMap_followPacket": "Uzamknúť pohľad na paket", "pathMap_followPacket": "Uzamknúť pohľad na paket",
"pathMap_unfollowPacket": "Odomknúť pohľad od paketu", "pathMap_unfollowPacket": "Odomknúť pohľad od paketu",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regiony",
"settings_regionSettingsSubtitle": "Zohľať uložené regiony",
"settings_regionManagement_screenTitle": "Regiónne vedenie",
"settings_regionNameHint": "Vstuptejte název regionu",
"settings_regionAddRegion": "Dodajte region",
"settings_regionFetchRegions": "Odberanie regionov z repeaterov",
"settings_regionFetchRegionsFail": "Nebylo našli žiadné regiony",
"settings_regionFetchRegionsAlreadyExists": "Tento oblast je už dodán",
"settings_regionName": "Regionné název",
"settings_regionDeleted": "Regionu zníteno",
"settings_deleteRegion": "Vyniknúcia regionu",
"settings_deleteRegionConfirm": "Odstraniť {region} z listy regionov?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Firmware",
"repeater_pathHashModeOption0": "0 - 1 byt",
"repeater_pathHashModeOption1": "1 - 2 bity",
"repeater_pathHashModeOption2": "2 - 3 bity",
"repeater_pathHashModeOption3": "3 - 4 bity",
"appSettings_batteryLipoHv": "LiPo HV (3.0-4.35V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: žiadné",
"channels_regionSelect_Title": "Vyberte region",
"channels_clearRegion": "Jasný údajný oblasť",
"chat_sendImage": "Vyslat obraz",
"chat_imagePickFailed": "Nesmiela jsem otvoriť tento obrázok",
"chat_receivedGif": "Odeslaný GIF",
"repeater_keySettings": "Zmena identititačných klíčov",
"repeater_keySettingsSubtitle": "Zmena prehľadného/osobného klíčového zbraň",
"repeater_prvKey": "Prívatná klíč",
"repeater_prvKeyHelper": "Nová prívna klíčová číslo pre repeater, 128-znáčlový šestnáctorýč.",
"repeater_generatePrvKey": "Vytvorenie príma kľúčového paru",
"repeater_stopGeneratingPrvKey": "Prerušenie hľadania klíčového paru",
"repeater_pubKey": "Public key",
"repeater_pubKeyHelper": "Táto je veľká klíč, ktorý sa vztahuje s generovaným manéčným klíčom. Nemožné je to ustanovit priateľne.",
"repeater_pubKeyPrefix": "Požadovaný prefiks",
"repeater_pubKeyPrefixHelper": "Zahľadnite sa veľkú publicenu klíču, ktorá začíná tými šestícih štiastkami. Očekivané potrebné pokusy: {tries}.",
"imageMessages_enableTitle": "Zabezpečte zobrazi",
"imageMessages_enableSubtitle": "Odeslanie obrazov pre mšťanie. Potrebujete jednorazové stáhnutie modelu obrazov.",
"imageMessages_modelSectionTitle": "Model v kínajme",
"imageMessages_downloadModel": "Stáhnie",
"imageMessages_cancelDownload": "Zastavenie",
"imageMessages_removeModel": "Vyhniť model",
"imageMessages_modelReady": "Čiačný",
"imageMessages_modelNotPublished": "Nesprávne vypisáné — tento prechovka nie môže jej stáhnuť.",
"imageMessages_downloadFailed": "Model s obrázkom nemôžno sťažovať.",
"imageMessages_autoProcessTitle": "Automaticky prepojiť slučaje.",
"imageMessages_autoProcessSubtitle": "Rekonstruujte každý obraz tak, ako pridá. Používajete okolo 2 GB pamäti pre drugú hodinu; odložte rekonstrukciu tým kliknutím.",
"imageSend_title": "Vyslat obraz",
"imageSend_cropNote": "Zmeneno na 512 × 512 · vzorýst nie byl zachovan",
"imageSend_originalSize": "Výborný text:\n\n\"Language is the road map of a culture. It tells us where to go as a society and helps us understand the world around us. Learning a new language opens up new perspectives and opportunities, allowing us to connect with people from different backgrounds and build bridges between cultures.\"\n\nSlovenská preklada:\n\n\"Jazyk je cesta pre zručnosti a odráža kultúru. Dá nám uvedomi, dokádať, kúdy sa pohladneme a pomáha našej společnosti poradiť sa v rôznych okolnosti a porozumieť o svetu kolem nás. Nauka nového jazyka otviera nové perspektívy a možnosti, čo pohybuje našu schopnosť sa spoluprávať s ľudmi z rôznych okolnosti a budovať mosty medzi kultúry.\"",
"imageSend_onAirSize": "V prúce",
"imageSend_quality": "Kvalita",
"imageSend_qualityStandard": "Stándard",
"imageSend_qualityHigh": "Vysoká",
"imageSend_packetsLabel": "Pakety",
"imageSend_airtimeLabel": "Čas na anteni",
"imageSend_sizeLabel": "Základ",
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paketov}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Nastavenia radio nezná",
"imageSend_radioUnknownBody": "Zkonečte s určým prijemcem, aby sa mohlo výpočet času, ktorý je v prúbežu.",
"imageSend_longSendTitle": "Dlouhá transmisia",
"imageSend_longSendBody": "Tento dôvod udrží kanál priamo na {duration}.",
"imageSend_floodNote": "Povodný routing: každý repeater v rozsahu zrňa každý paket, takže kanál zostá zajetší dlouше, než tu.",
"imageSend_parityTitle": "Poučný paket pre očudenie",
"imageSend_paritySubtitle": "Jedný dodatný paket. Skúsené súhlasy nie sa uznávaú, takže to umožňuje prechovávaču opraviť obraz, pokiaľ sa jedný paket niebyl ztrácený.",
"imageSend_send": "Odeslať",
"imageSend_cancel": "Zastavenie",
"imageSend_encodeFailed": "Tento obráz nie byla byla schována.",
"imageSend_codecDownloading": "Model prehľadovania prebeža stále stále stále prechádza.",
"imageSend_codecUnavailable": "Zobrazenie nie je dostupný na tomto zariadení.",
"imageSend_codecDisabled": "Zobrazy s prešteľkami sú v nastavení vypnené.",
"imageSend_deviceUnsupported": "Tento radiok nie môže odeslať obrazové pakety. Podeďte správne určité zařízení s oprogramovaním spolupráce 13 alebo novším.",
"imageSend_directMessagesUnsupported": "Obrazy sa prechádzajú jako skupinové údaje, preto sa ich môžu odeslať len na kanál — nie v príbeh sa.",
"imageSend_tooLarge": "Tento obraz byl skodyrovany do viac paketov, ako umožňuje si sítka format.",
"imageSend_sentConfirmation": "Obrázok odoslaný ako {count} {count, plural, =1{paket} other{paketov}}.",
"imageSend_sendFailed": "Obraz nemôžlo odeslať: {error}",
"imageSend_sendingProgress": "Vysyťanie obrazu — pakét {sent} z {total}",
"receivedImage_senderPrefix": "Vęzlo {prefix}",
"receivedImage_incoming": "Otrzymané {received} z {total} pakotí",
"receivedImage_queued": "Čekanie na dekódovanie",
"receivedImage_tapToDecode": "Kliknite, aby dekódovať",
"receivedImage_decoding": "Rekonstrukcia… o čoľve 1 s",
"receivedImage_incomplete": "Obraz nieješťajný — {received} z {total} paciek príšlo",
"receivedImage_corrupt": "Obraz nemôžu byť prepojený",
"receivedImage_decoderMissing": "Otrzymaná slúba — dekódovanie slúby je vypadejce",
"receivedImage_evicted": "Obraz nemá uložený",
"receivedImage_retry": "Zkôr to opítajte",
"receivedImage_decodeAgain": "Zdeľte sa znovu",
"receivedImage_openSettings": "Založte",
"receivedImage_tapToProcess": "Ukliknite, aby sa príjomili",
"receivedImage_awaiting": "{bytes} bajtov · {packets} {packets, plural, =1{paket} other{paketov}}",
"imageSend_secondsValue": "{seconds} s",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.", "settings_locationInvalid": "Neveljavna zemeljska širina ali dolžina.",
"settings_latitude": "Širina", "settings_latitude": "Širina",
"settings_longitude": "Dolžina", "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_privacyMode": "Zasebnost",
"settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih", "settings_privacyModeSubtitle": "Skrita imena/lokacije v oglasih",
"settings_privacyModeToggle": "Omogoči način zasebnosti, da skrijemo tvoje ime in lokacijo 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_last6Hours": "Zadnjih 6 ur",
"appSettings_last24Hours": "Zadnjih 24 ur", "appSettings_last24Hours": "Zadnjih 24 ur",
"appSettings_lastWeek": "Prejšnji teden", "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_offlineMapCache": "Shramba zemljevidov brez povezave",
"appSettings_noAreaSelected": "Območje ni izbrano", "appSettings_noAreaSelected": "Območje ni izbrano",
"appSettings_areaSelectedZoom": "Izbrano območje (povečava {minZoom}-{maxZoom})", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1107,7 +1172,7 @@
"repeater_advancedSettings": "Napredno", "repeater_advancedSettings": "Napredno",
"repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike", "repeater_advancedSettingsSubtitle": "Gumbi za nastavljanje za izkušene uporabnike",
"repeater_pathHashMode": "Način ustvarjanja hash-a poti", "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_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_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", "repeater_directTxDelay": "Neposredni časovno odlašanje",
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Brez lokacije", "pathMap_noLocation": "Brez lokacije",
"pathMap_followPacket": "Zakleni pogled na paket", "pathMap_followPacket": "Zakleni pogled na paket",
"pathMap_unfollowPacket": "Odkleni pogled od paketa", "pathMap_unfollowPacket": "Odkleni pogled od paketa",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regioni",
"settings_regionSettingsSubtitle": "Upravljanje opisanih regionov",
"settings_regionManagement_screenTitle": "Regijonsko upravljanje",
"settings_regionNameHint": "Naslov občine",
"settings_regionAddRegion": "Dodaj region",
"settings_regionFetchRegions": "Pobražujte izvajne področja iz ponavljalnikov",
"settings_regionFetchRegionsFail": "Nižnih občin ni izkrali",
"settings_regionFetchRegionsAlreadyExists": "Taj oblasi je več pomanjkan.",
"settings_regionName": "Naziv Regiona",
"settings_regionDeleted": "Oblikačena regija",
"settings_deleteRegion": "Prihoda Region",
"settings_deleteRegionConfirm": "Ostani {region} iz listy regionov?",
"settings_infoHardware": "Hardver",
"settings_infoFirmware": "Pravilnik",
"repeater_pathHashModeOption0": "0 - 1 bajt",
"repeater_pathHashModeOption1": "1 - 2 bite",
"repeater_pathHashModeOption2": "2 - 3 bite",
"repeater_pathHashModeOption3": "3 - 4 bite",
"appSettings_batteryLipoHv": "LiPo HV (3,04,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"
} }
+289 -2
View File
@@ -100,6 +100,8 @@
"settings_locationInvalid": "Ogiltig latitud eller longitud.", "settings_locationInvalid": "Ogiltig latitud eller longitud.",
"settings_latitude": "Latitud", "settings_latitude": "Latitud",
"settings_longitude": "Längdgrad", "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_privacyMode": "Privatläge",
"settings_privacyModeSubtitle": "Dölj namn/plats i annonser", "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.", "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_last6Hours": "De senaste 6 timmarna",
"appSettings_last24Hours": "De senaste 24 timmarna", "appSettings_last24Hours": "De senaste 24 timmarna",
"appSettings_lastWeek": "Förra veckan", "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_offlineMapCache": "Offline Kartcache",
"appSettings_noAreaSelected": "Ingen area markerad", "appSettings_noAreaSelected": "Ingen area markerad",
"appSettings_areaSelectedZoom": "Område markerat (zoom {minZoom}-{maxZoom})", "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": "N {north}, S {south}, E {east}, W {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1107,7 +1172,7 @@
"repeater_advancedSettings": "Avancerad", "repeater_advancedSettings": "Avancerad",
"repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare", "repeater_advancedSettingsSubtitle": "Ställjusteringsknappar för erfarna användare",
"repeater_pathHashMode": "Hash-läge för sökväg", "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_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_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", "repeater_directTxDelay": "Direkt TX-fördröjning",
@@ -2540,5 +2605,227 @@
"pathMap_noLocation": "Ingen position", "pathMap_noLocation": "Ingen position",
"pathMap_unfollowPacket": "Lås upp vy från paket", "pathMap_unfollowPacket": "Lås upp vy från paket",
"pathMap_followPacket": "Lås vy till paket", "pathMap_followPacket": "Lås vy till paket",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Regioner",
"settings_regionSettingsSubtitle": "Hämtade regioner",
"settings_regionManagement_screenTitle": "Regionförvaltning",
"settings_regionNameHint": "Enter regionnamn",
"settings_regionAddRegion": "Lag till region",
"settings_regionFetchRegions": "Hämtar regioner från repeater",
"settings_regionFetchRegionsFail": "Ingen region hittades",
"settings_regionFetchRegionsAlreadyExists": "Denna region har redan lagt till",
"settings_regionName": "Regionnavn",
"settings_regionDeleted": "Regionens kallt offentlig",
"settings_deleteRegion": "Förhålla regio",
"settings_deleteRegionConfirm": "Fjärna \"{region}\" ur regionlistan?",
"settings_infoHardware": "Hardware",
"settings_infoFirmware": "Föredata",
"repeater_pathHashModeOption0": "0 - 1 byte",
"repeater_pathHashModeOption1": "1 - 2 byte",
"repeater_pathHashModeOption2": "2 - 3 byte",
"repeater_pathHashModeOption3": "3 - 4 byte",
"appSettings_batteryLipoHv": "LiPo HV (3,0-4,35 V)",
"channels_regionSetTo": "Region: {region}",
"channels_regionNotSet": "Region: ingen",
"channels_regionSelect_Title": "Välj en region",
"channels_clearRegion": "klar region",
"chat_sendImage": "Send vagnbild",
"chat_imagePickFailed": "Kunde inte öppna det bildet",
"chat_receivedGif": "Mottagit en GIF",
"repeater_keySettings": "Ändra identitetsnyckel",
"repeater_keySettingsSubtitle": "Vänd nya offentliga/privata nyckelpar",
"repeater_prvKey": "Privat nyckel",
"repeater_prvKeyHelper": "En ny privat nyckel för repeatern, en hexsträng med 128 tecken.",
"repeater_generatePrvKey": "Generera en kära nyckelpaar",
"repeater_stopGeneratingPrvKey": "Avbröka sökande efter nyckelpa",
"repeater_pubKey": "Öffentlig nyckel",
"repeater_pubKeyHelper": "Detta är den offentliga nyckeln som tillhör genererad privaten nyckel. Du kan inte setta den direkt.",
"repeater_pubKeyPrefix": "Önskad prefix",
"repeater_pubKeyPrefixHelper": "Hitta en offentlig nyckel som börjar med dessa hexadecimala siffror. Förväntad antal försök: {tries}.",
"imageMessages_enableTitle": "Bildermeddelanden",
"imageMessages_enableSubtitle": "Send bilder över mönstret. Det kräver en enkelt bildmodellhämtning.",
"imageMessages_modelSectionTitle": "Bildmodell",
"imageMessages_downloadModel": "Download",
"imageMessages_cancelDownload": "Avbud",
"imageMessages_removeModel": "Förhåll dig från modellen",
"imageMessages_modelReady": "Färdig",
"imageMessages_modelNotPublished": "Ingen publikerad än — denna byggnads kan inte laddas ner av det.",
"imageMessages_downloadFailed": "Bildmodellen kunde inte laddas ner.",
"imageMessages_autoProcessTitle": "Automatiska bilder automatiserar",
"imageMessages_autoProcessSubtitle": "Rekonstruera varje bild så snart den ankommer. Använder cirka 2 GB minne varje gång; låt det omför att rekonstruera genom ett tap.",
"imageSend_title": "Send vagnbild",
"imageSend_cropNote": "Reserverat till 512 × 512 · förhållandet mellan storlek inte bevarat",
"imageSend_originalSize": "Ovanstående text (engelska)\n\nTranslation (svenskt):",
"imageSend_onAirSize": "Underför hjärnan",
"imageSend_quality": "Kvalitet",
"imageSend_qualityStandard": "Standard",
"imageSend_qualityHigh": "Hög",
"imageSend_packetsLabel": "Paket",
"imageSend_airtimeLabel": "Tid i övrighet",
"imageSend_sizeLabel": "Ladning",
"imageSend_packetsCount": "{count} {count, plural, =1{paket} other{paket}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Radioindikatorer inte kända",
"imageSend_radioUnknownBody": "Förkoppna till ett enhet så kan tiden på air beräknas.",
"imageSend_longSendTitle": "Långförsending",
"imageSend_longSendBody": "Detta kommer att bevara kanalen roughly {duration}.",
"imageSend_floodNote": "Flodledning: varje repeater i ramen retransmitterar varje paket, så kan kanalen vara på arbete långare än den här tiden.",
"imageSend_parityTitle": "Omedlningspaket",
"imageSend_paritySubtitle": "En ytterligare paket. Gruppmeddelanden inte akceras, så den här tar hänsyn till att mottar kan återuppbygga bilden om ett enkelt paket förlorats.",
"imageSend_send": "Sänd",
"imageSend_cancel": "Avbud",
"imageSend_encodeFailed": "Denna bild kan inte kodas.",
"imageSend_codecDownloading": "Bildmodellen downloads fortfarande.",
"imageSend_codecUnavailable": "Bildsendning är inte tillgänglig på detta enhet.",
"imageSend_codecDisabled": "Bildmeddelanden är offna i inställningarna.",
"imageSend_deviceUnsupported": "Denna radi kan inte senda bildpaket. Använd ett äventyrsgerät med companionsystemfirmware 13 eller nyare.",
"imageSend_directMessagesUnsupported": "Bilder senders som gruppod, så de kan endast skickas till ett kanal inte i en direkt meddelning.",
"imageSend_tooLarge": "Bilden kodades till mer paket än meshformaten tillåter.",
"imageSend_sentConfirmation": "Bilden skickades som {count} {count, plural, =1{paket} other{paket}}.",
"imageSend_sendFailed": "Bilden kunde inte skickas: {error}",
"imageSend_sendingProgress": "Bild sendning — paket {sent} av {total}",
"receivedImage_senderPrefix": "Nod {prefix}",
"receivedImage_incoming": "VANTA {received} av {total} paket",
"receivedImage_queued": "Att väta att decodera",
"receivedImage_tapToDecode": "Tap för att decodera",
"receivedImage_decoding": "Rekonstruktion… om 1 sekund",
"receivedImage_incomplete": "Bilden är okomplett — {received} av {total} paket har ankomit",
"receivedImage_corrupt": "Bilden kunde inte rekonstrueras",
"receivedImage_decoderMissing": "Bild mottagen — bilddekoderingen är avbrott",
"receivedImage_evicted": "Bild inte lagrad längre",
"receivedImage_retry": "Omförs igen",
"receivedImage_decodeAgain": "Kodera igen",
"receivedImage_openSettings": "Skapa en uppställning",
"receivedImage_tapToProcess": "Tap för att behandla",
"receivedImage_awaiting": "{bytes} byte · {packets} {packets, plural, =1{paket} other{paket}}",
"imageSend_secondsValue": "{seconds} sekunder",
"imageSend_minutesSecondsValue": "{minutes} min {seconds} s"
} }
+289 -2
View File
@@ -101,6 +101,8 @@
"settings_locationInvalid": "Некоректна широта або довгота.", "settings_locationInvalid": "Некоректна широта або довгота.",
"settings_latitude": "Широта", "settings_latitude": "Широта",
"settings_longitude": "Довгота", "settings_longitude": "Довгота",
"settings_autoZeroHopAdvertOnGpsUpdate": "Автооголошення без хопів при оновленні GPS",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "Коли GPS-локація змінюється, надсилати оголошення без хопів (потрібна геопозиція в оголошенні).",
"settings_privacyMode": "Режим приватності", "settings_privacyMode": "Режим приватності",
"settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях", "settings_privacyModeSubtitle": "Приховати ім'я/геопозицію в оголошеннях",
"settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.", "settings_privacyModeToggle": "Увімкніть режим приватності, щоб приховати своє ім'я та геопозицію в оголошеннях.",
@@ -242,6 +244,19 @@
"appSettings_last6Hours": "Останні 6 годин", "appSettings_last6Hours": "Останні 6 годин",
"appSettings_last24Hours": "Останні 24 години", "appSettings_last24Hours": "Останні 24 години",
"appSettings_lastWeek": "Минулий тиждень", "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_offlineMapCache": "Офлайн-кеш карти",
"appSettings_noAreaSelected": "Область не вибрано", "appSettings_noAreaSelected": "Область не вибрано",
"appSettings_areaSelectedZoom": "Вибрана область (зум {minZoom}-{maxZoom})", "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": "Пн {north}, Пд {south}, Сх {east}, Зх {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1117,7 +1182,7 @@
"repeater_advancedSettings": "Просунутий", "repeater_advancedSettings": "Просунутий",
"repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів", "repeater_advancedSettingsSubtitle": "Регулювальні ручки для досвідчених операторів",
"repeater_pathHashMode": "Режим хешування шляху", "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_txDelay": "Затримка у Flood, штат Техас",
"repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.", "repeater_txDelayHelper": "Повторне надсилання з урахуванням навантаження від потоків транспорту, як множник від часу передачі пакета (0-2, за замовчуванням 0,5). Чим вище значення, тим менше конфліктів, але повільніше передавання.",
"repeater_directTxDelay": "Пряме затримка TX", "repeater_directTxDelay": "Пряме затримка TX",
@@ -2520,5 +2585,227 @@
"pathMap_noLocation": "Без геопозиції", "pathMap_noLocation": "Без геопозиції",
"pathMap_followPacket": "Прив'язати вигляд до пакету", "pathMap_followPacket": "Прив'язати вигляд до пакету",
"pathMap_unfollowPacket": "Відв'язати вигляд від пакету", "pathMap_unfollowPacket": "Відв'язати вигляд від пакету",
"pathMap_gpsCount": "{confirmed}/{total} GPS" "pathMap_gpsCount": "{confirmed}/{total} GPS",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "Ріси",
"settings_regionSettingsSubtitle": "Управляти збережених регіонів",
"settings_regionManagement_screenTitle": "Рégіональний управління",
"settings_regionNameHint": "Назва регіону",
"settings_regionAddRegion": "Добачить регіон",
"settings_regionFetchRegions": "Збираємо регіони з репітерів",
"settings_regionFetchRegionsFail": "Ні відкриті регіони",
"settings_regionFetchRegionsAlreadyExists": "Цей регіон вже було доданий",
"settings_regionName": "Ріонна назва",
"settings_regionDeleted": "Ріон витертений",
"settings_deleteRegion": "Видалення регіону",
"settings_deleteRegionConfirm": "Вирішіть \"{region}\" з списку регіонів?",
"settings_infoHardware": "Оборудовання",
"settings_infoFirmware": "Програмне вміст (фirmware)",
"repeater_pathHashModeOption0": "0 - 1 байт",
"repeater_pathHashModeOption1": "1 - 2 байти",
"repeater_pathHashModeOption2": "2 - 3 байти",
"repeater_pathHashModeOption3": "3 - 4 байта",
"appSettings_batteryLipoHv": "ЛиPo HV (3,0-4,35 В)",
"channels_regionSetTo": "Ріон: {region}",
"channels_regionNotSet": "Ріон: нема",
"channels_regionSelect_Title": "Вybірте регіон",
"channels_clearRegion": "Чистя зона",
"chat_sendImage": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
"chat_imagePickFailed": "Неможливо відкрити це изображення",
"chat_receivedGif": "Підймовано GIF",
"repeater_keySettings": "Зміна ключів ідентифікації",
"repeater_keySettingsSubtitle": "Замініть публічний/приватний ключ",
"repeater_prvKey": "Приватний ключ",
"repeater_prvKeyHelper": "Нова приватна ключ для репітера, 128-символова штучна штука в шестнадцатеричному форматі.",
"repeater_generatePrvKey": "Синхронізujте ключовий пару",
"repeater_stopGeneratingPrvKey": "Прервити шук ключового пару",
"repeater_pubKey": "Публічна ключ",
"repeater_pubKeyHelper": "Це публична ключ, який пов'язано з створеною приватною ключом. Ви не можете встановлювати його напрямую.",
"repeater_pubKeyPrefix": "Високорейтингове прикінання",
"repeater_pubKeyPrefixHelper": "Найдіть відповідний открытий ключ, який починається ціми шестнадцатыми цифрами. Ожидається кількість потрібних попыток: {tries}.",
"imageMessages_enableTitle": "Підтримка зображень для посилання",
"imageMessages_enableSubtitle": "Пошукуєте изображення через сеть. Вимagaєте однократне скачання моделю изображення.",
"imageMessages_modelSectionTitle": "Модель для зображень",
"imageMessages_downloadModel": "Скачання",
"imageMessages_cancelDownload": "Завершити",
"imageMessages_removeModel": "Видалим модель",
"imageMessages_modelReady": "Готово",
"imageMessages_modelNotPublished": "Щоб не було опубліковано — це версія не дозволяє зробити скачання.",
"imageMessages_downloadFailed": "Модель для зображень не можна сказати.",
"imageMessages_autoProcessTitle": "Автоматично оброблювати obraзи",
"imageMessages_autoProcessSubtitle": "Кожна картинка повинні бути відтворена на мій момент прибуття. Використовує приблизно 2 ГБ пам'яті на кожен раз; відключіть від відтворення за допомогою кlikу.",
"imageSend_title": "Нашкоду зовні\n\n(or more naturally, depending on context)\n\nПошукуєте изображення",
"imageSend_cropNote": "Манітовано до 512 × 512 · соотношення варість не зберігається",
"imageSend_originalSize": "Оригінал",
"imageSend_onAirSize": "На ері",
"imageSend_quality": "Качество",
"imageSend_qualityStandard": "Стандарт",
"imageSend_qualityHigh": "Висок",
"imageSend_packetsLabel": "Пакети",
"imageSend_airtimeLabel": "Час на ері",
"imageSend_sizeLabel": "Підлог",
"imageSend_packetsCount": "{count} {count, plural, one{пакет} few{пакети} many{пакетів} other{пакета}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "Настройки радио неизвестні",
"imageSend_radioUnknownBody": "Залучіться до пристрою, щоб можна було розрахунювати час в етері.",
"imageSend_longSendTitle": "Длуга передача",
"imageSend_longSendBody": "Це забезпечить канал на approximativno {duration} хвилин.",
"imageSend_floodNote": "Рухування потоків: кожен ретрансатор у діапазоні відправлює кожен пакет, тому канал залишається занятшим довше, ніж це.",
"imageSend_parityTitle": "Пакет відновлення",
"imageSend_paritySubtitle": "Один дополний пакет. Групові посилання не відповідають, тому це дозволяє відвідувачу відтворити зображення, якщо один пакет втрачений.",
"imageSend_send": "Пошутить",
"imageSend_cancel": "Завершити",
"imageSend_encodeFailed": "Це изображення не може бути кодировано.",
"imageSend_codecDownloading": "Модель для відтворення изображень ще збирається.",
"imageSend_codecUnavailable": "Постій для відправлення изображень на цьому пристрої не доступний.",
"imageSend_codecDisabled": "Пictesovani posyly wyключені в настройках.",
"imageSend_deviceUnsupported": "Цей радіопередач не може отправляти пакети зображень. Зконнете пристрої, що працює з обладнанням керемонітом компаньєр 13 або новшим.",
"imageSend_directMessagesUnsupported": "Фотографії передаються як дані групою, тому їх можна відправляти лише на канал — не в прямому посилання.",
"imageSend_tooLarge": "Це изображення кодує більше пакітів, ніж дозволяє формат сети.",
"imageSend_sentConfirmation": "Зображення надіслано як {count} {count, plural, =1{пакет} other{пакетів}}.",
"imageSend_sendFailed": "Изображення не можна відправити: {error}",
"imageSend_sendingProgress": "Пошускаємо изображення — пакет {sent} з {total}",
"receivedImage_senderPrefix": "Нод {prefix}",
"receivedImage_incoming": "Підходяче: {received} з {total} пакітів",
"receivedImage_queued": "Очікування розслівання коду",
"receivedImage_tapToDecode": "Кliknite для декодування",
"receivedImage_decoding": "Повноювання… за прибуття 1 с.",
"receivedImage_incomplete": "Образок не повний — {received} з {total} пакітів прибавлено",
"receivedImage_corrupt": "Obraz не можна відтворити",
"receivedImage_decoderMissing": "Під час приємки — розшифровка зображення не вдалося.",
"receivedImage_evicted": "Изображення не зберігається",
"receivedImage_retry": "Попробуй ще раз",
"receivedImage_decodeAgain": "Декодуй ще раз",
"receivedImage_openSettings": "Заведіть",
"receivedImage_tapToProcess": "Кликніть для обробки",
"receivedImage_awaiting": "{bytes} байтів · {packets} {packets, plural, =1{пакет} other{пакетів}}",
"imageSend_secondsValue": "{seconds} секунди",
"imageSend_minutesSecondsValue": "{minutes} хв {seconds} с"
} }
+289 -2
View File
@@ -105,6 +105,8 @@
"settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。", "settings_locationIntervalInvalid": "间隔时间必须至少为 60 秒,但不超过 86400 秒。",
"settings_latitude": "纬度", "settings_latitude": "纬度",
"settings_longitude": "经度", "settings_longitude": "经度",
"settings_autoZeroHopAdvertOnGpsUpdate": "GPS 更新时自动发送零跳广告",
"settings_autoZeroHopAdvertOnGpsUpdateSubtitle": "当 GPS 位置变化时,发送零跳广告(需要在广告中包含位置)。",
"settings_privacyMode": "隐私模式", "settings_privacyMode": "隐私模式",
"settings_privacyModeSubtitle": "在广告中隐藏姓名/位置", "settings_privacyModeSubtitle": "在广告中隐藏姓名/位置",
"settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。", "settings_privacyModeToggle": "切换隐私模式以在广告中隐藏姓名和位置,保护个人信息。",
@@ -254,6 +256,19 @@
"appSettings_last6Hours": "过去6小时", "appSettings_last6Hours": "过去6小时",
"appSettings_last24Hours": "过去24小时", "appSettings_last24Hours": "过去24小时",
"appSettings_lastWeek": "上周", "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_offlineMapCache": "离线地图缓存",
"appSettings_noAreaSelected": "未选择任何区域", "appSettings_noAreaSelected": "未选择任何区域",
"appSettings_areaSelectedZoom": "已选择区域(缩放 {minZoom} - {maxZoom}", "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": "北 {north}, 南 {south}, 东 {east}, 西 {west}",
"@mapCache_boundsLabel": { "@mapCache_boundsLabel": {
"placeholders": { "placeholders": {
@@ -1137,7 +1202,7 @@
"repeater_advancedSettings": "高级", "repeater_advancedSettings": "高级",
"repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮", "repeater_advancedSettingsSubtitle": "高级操作员使用的调节旋钮",
"repeater_pathHashMode": "路径哈希模式", "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_txDelay": "洪水(德克萨斯州)延误",
"repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。", "repeater_txDelayHelper": "对于洪水流量,重新传输间隔应设置为包的传输时间(0-2,默认值为0.5)的倍数。 较高的值意味着更少的冲突,但传输速度会变慢。",
"repeater_directTxDelay": "直接的 TX 延迟", "repeater_directTxDelay": "直接的 TX 延迟",
@@ -2545,5 +2610,227 @@
"pathMap_followPacket": "锁定视图跟随数据包", "pathMap_followPacket": "锁定视图跟随数据包",
"pathMap_unfollowPacket": "解锁视图跟随", "pathMap_unfollowPacket": "解锁视图跟随",
"pathMap_gpsCount": "{confirmed}/{total} GPS", "pathMap_gpsCount": "{confirmed}/{total} GPS",
"pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}" "pathMap_partialAnimation": "{count, plural, =1{1 跳无位置信息 — 显示的路径不完整} other{{count} 跳无位置信息 — 显示的路径不完整}}",
"@settings_deleteRegionConfirm": {
"placeholders": {
"region": {
"type": "String"
}
}
},
"@channels_regionSetTo": {
"placeholders": {
"region": {
"type": "String",
"example": "de-mitte"
}
}
},
"@chat_imagePickFailed": {
"description": "Shown when picking or reading a photo to send fails"
},
"@repeater_pubKeyPrefixHelper": {
"placeholders": {
"tries": {
"type": "int"
}
}
},
"@imageSend_packetsCount": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_range": {
"placeholders": {
"min": {
"type": "String"
},
"max": {
"type": "String"
}
}
},
"@imageSend_longSendBody": {
"placeholders": {
"duration": {
"type": "String"
}
}
},
"@imageSend_sentConfirmation": {
"placeholders": {
"count": {
"type": "int"
}
}
},
"@imageSend_sendFailed": {
"placeholders": {
"error": {
"type": "String"
}
}
},
"@imageSend_sendingProgress": {
"placeholders": {
"sent": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_senderPrefix": {
"placeholders": {
"prefix": {
"type": "String"
}
}
},
"@receivedImage_incoming": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_incomplete": {
"placeholders": {
"received": {
"type": "int"
},
"total": {
"type": "int"
}
}
},
"@receivedImage_awaiting": {
"placeholders": {
"bytes": {
"type": "int"
},
"packets": {
"type": "int"
}
}
},
"@imageSend_secondsValue": {
"placeholders": {
"seconds": {
"type": "String"
}
}
},
"@imageSend_minutesSecondsValue": {
"placeholders": {
"minutes": {
"type": "String"
},
"seconds": {
"type": "String"
}
}
},
"settings_regionSettings": "地区",
"settings_regionSettingsSubtitle": "管理已存储的区域",
"settings_regionManagement_screenTitle": "区域管理",
"settings_regionNameHint": "区域名称",
"settings_regionAddRegion": "增加地区",
"settings_regionFetchRegions": "从中继器获取区域",
"settings_regionFetchRegionsFail": "未发现任何地区",
"settings_regionFetchRegionsAlreadyExists": "该地区已被添加",
"settings_regionName": "地区名称",
"settings_regionDeleted": "区域已删除",
"settings_deleteRegion": "删除区域",
"settings_deleteRegionConfirm": "从 {region} 列表中删除?",
"settings_infoHardware": "硬件",
"settings_infoFirmware": "固件",
"repeater_pathHashModeOption0": "0 - 1 字节",
"repeater_pathHashModeOption1": "1 - 2 字节",
"repeater_pathHashModeOption2": "2 - 3 字节",
"repeater_pathHashModeOption3": "3 - 4 字节",
"appSettings_batteryLipoHv": "锂多压电源(3.0-4.35伏)",
"channels_regionSetTo": "地区:{region}",
"channels_regionNotSet": "地区:无",
"channels_regionSelect_Title": "选择一个地区",
"channels_clearRegion": "无碍区域",
"chat_sendImage": "发送图片",
"chat_imagePickFailed": "无法打开该图片",
"chat_receivedGif": "收到一个GIF",
"repeater_keySettings": "更改身份密钥",
"repeater_keySettingsSubtitle": "更换公钥/私钥对",
"repeater_prvKey": "私钥",
"repeater_prvKeyHelper": "中继器的新私钥,为一个128位的十六进制字符串。",
"repeater_generatePrvKey": "生成一个随机的密钥对",
"repeater_stopGeneratingPrvKey": "中断密钥对搜索",
"repeater_pubKey": "公钥",
"repeater_pubKeyHelper": "这是与生成的私钥配对的公钥。您不能直接设置此内容。",
"repeater_pubKeyPrefix": "期望的前缀",
"repeater_pubKeyPrefixHelper": "找到一组以这些十六进制数字开头的公钥。预计尝试次数:{tries}",
"imageMessages_enableTitle": "启用图片消息",
"imageMessages_enableSubtitle": "通过网格发送图像。需要一次性下载图像模型。",
"imageMessages_modelSectionTitle": "图像模型",
"imageMessages_downloadModel": "下载",
"imageMessages_cancelDownload": "取消",
"imageMessages_removeModel": "移除模型",
"imageMessages_modelReady": "准备就绪",
"imageMessages_modelNotPublished": "尚未发布——此版本无法下载该内容。",
"imageMessages_downloadFailed": "图像模型无法下载。",
"imageMessages_autoProcessTitle": "自动处理图像",
"imageMessages_autoProcessSubtitle": "每次图像到达后立即重建,每次需要约2GB的内存;若想关闭重建功能,可通过点击取消开启。",
"imageSend_title": "发送图片",
"imageSend_cropNote": "调整大小至512 × 512 · 宽高比未保留",
"imageSend_originalSize": "原文",
"imageSend_onAirSize": "直播中",
"imageSend_quality": "质量",
"imageSend_qualityStandard": "标准",
"imageSend_qualityHigh": "高",
"imageSend_packetsLabel": "数据包",
"imageSend_airtimeLabel": "播出时间",
"imageSend_sizeLabel": "有效载荷",
"imageSend_packetsCount": "{count} {count, plural, =1{个数据包} other{个数据包}}",
"imageSend_range": "{min}{max}",
"imageSend_unknownValue": "—",
"imageSend_radioUnknownTitle": "无线电设置未知",
"imageSend_radioUnknownBody": "连接设备以计算播出时间。",
"imageSend_longSendTitle": "长距离传输",
"imageSend_longSendBody": "这将维持频道大约 {duration} 时间。",
"imageSend_floodNote": "洪水路由:每个中继站都会转发每个数据包,因此信道比这段时间更长地处于繁忙状态。",
"imageSend_parityTitle": "恢复包",
"imageSend_paritySubtitle": "多一个数据包。群发消息不被确认,因此如果某个数据包丢失,接收方可以重新构建图像。",
"imageSend_send": "发送",
"imageSend_cancel": "取消",
"imageSend_encodeFailed": "此图像无法编码。",
"imageSend_codecDownloading": "图像模型仍在下载中。",
"imageSend_codecUnavailable": "此设备无法发送图片。",
"imageSend_codecDisabled": "图片消息在设置中已关闭。",
"imageSend_deviceUnsupported": "此无线电无法发送图像数据包。请连接运行伴侣固件13或更高版本的设备。",
"imageSend_directMessagesUnsupported": "图片作为群组数据传输,因此只能发送到频道,而不能直接发送给个人。",
"imageSend_tooLarge": "该图像编码的数据包数量超过了网格格式所允许的范围。",
"imageSend_sentConfirmation": "图像已作为 {count} {count, plural, =1{个数据包} other{个数据包}} 发送。",
"imageSend_sendFailed": "无法发送图片:{error}",
"imageSend_sendingProgress": "发送图像 — 已发送 {sent} 包含 {total}",
"receivedImage_senderPrefix": "节点 {prefix}",
"receivedImage_incoming": "已收到的 {received} 个包含在 {total} 个包裹中",
"receivedImage_queued": "等待解码",
"receivedImage_tapToDecode": "点击解锁",
"receivedImage_decoding": "重建……大约1秒",
"receivedImage_incomplete": "图片不完整 — {received} 已收到 {total} 个数据包",
"receivedImage_corrupt": "图像无法重建",
"receivedImage_decoderMissing": "收到图片 — 图像解码失败",
"receivedImage_evicted": "图片已不再存储",
"receivedImage_retry": "再试一次",
"receivedImage_decodeAgain": "重新解码",
"receivedImage_openSettings": "建立",
"receivedImage_tapToProcess": "点击处理",
"receivedImage_awaiting": "{bytes} 字节 · {packets} {packets, plural, =1{个数据包} other{个数据包}}",
"imageSend_secondsValue": "{seconds} 秒",
"imageSend_minutesSecondsValue": "{minutes} 分 {seconds} 秒"
} }
+20 -3
View File
@@ -1,4 +1,5 @@
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/path_helper.dart';
import '../models/contact.dart'; import '../models/contact.dart';
import 'app_localizations.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 != null) {
if (pathOverride! < 0) return l10n.chat_floodForced; if (pathOverride! < 0) return l10n.chat_floodForced;
if (pathOverride == 0) return l10n.chat_directForced; 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.channelPath_floodPath;
if (pathLength == 0) return l10n.chat_direct; 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;
} }
} }
+234 -13
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@@ -9,7 +11,13 @@ import 'screens/chrome_required_screen.dart';
import 'utils/platform_info.dart'; import 'utils/platform_info.dart';
import 'connector/meshcore_connector.dart'; import 'connector/meshcore_connector.dart';
import 'models/image_codec_support.dart';
import 'screens/scanner_screen.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/storage_service.dart';
import 'services/message_retry_service.dart'; import 'services/message_retry_service.dart';
import 'services/path_history_service.dart'; import 'services/path_history_service.dart';
@@ -26,6 +34,7 @@ import 'services/timeout_prediction_service.dart';
import 'storage/prefs_manager.dart'; import 'storage/prefs_manager.dart';
import 'theme/mesh_theme.dart'; import 'theme/mesh_theme.dart';
import 'utils/app_logger.dart'; import 'utils/app_logger.dart';
import 'widgets/image_send_codec_binding.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@@ -51,15 +60,60 @@ void main() async {
final bleDebugLogService = BleDebugLogService(); final bleDebugLogService = BleDebugLogService();
final appDebugLogService = AppDebugLogService(); final appDebugLogService = AppDebugLogService();
final backgroundService = BackgroundService(); final backgroundService = BackgroundService();
final mapTileCacheService = MapTileCacheService(); final mapTileCacheService = MapTileCacheService(
appSettingsService: appSettingsService,
);
final chatTextScaleService = ChatTextScaleService(); final chatTextScaleService = ChatTextScaleService();
final translationService = TranslationService(appSettingsService); final translationService = TranslationService(appSettingsService);
final uiViewStateService = UiViewStateService(); final uiViewStateService = UiViewStateService();
final timeoutPredictionService = TimeoutPredictionService(storage); 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(); 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 // Initialize app logger
appLogger.initialize( appLogger.initialize(
appDebugLogService, appDebugLogService,
@@ -77,6 +131,8 @@ void main() async {
await chatTextScaleService.initialize(); await chatTextScaleService.initialize();
await translationService.refreshDownloadedModels(); await translationService.refreshDownloadedModels();
await imageCodecService.refreshDownloadedModels();
await receivedImageStore.load();
await uiViewStateService.initialize(); await uiViewStateService.initialize();
await timeoutPredictionService.initialize(); await timeoutPredictionService.initialize();
@@ -90,6 +146,12 @@ void main() async {
appDebugLogService: appDebugLogService, appDebugLogService: appDebugLogService,
backgroundService: backgroundService, backgroundService: backgroundService,
timeoutPredictionService: timeoutPredictionService, 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(); await connector.loadContactCache();
@@ -114,10 +176,107 @@ void main() async {
translationService: translationService, translationService: translationService,
uiViewStateService: uiViewStateService, uiViewStateService: uiViewStateService,
timeoutPredictionService: timeoutPredictionService, 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() { void _registerThirdPartyLicenses() {
LicenseRegistry.addLicense(() async* { LicenseRegistry.addLicense(() async* {
yield const LicenseEntryWithLineBreaks( 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 MeshCoreConnector connector;
final MessageRetryService retryService; final MessageRetryService retryService;
final PathHistoryService pathHistoryService; final PathHistoryService pathHistoryService;
@@ -152,6 +311,9 @@ class MeshCoreApp extends StatelessWidget {
final TranslationService translationService; final TranslationService translationService;
final UiViewStateService uiViewStateService; final UiViewStateService uiViewStateService;
final TimeoutPredictionService timeoutPredictionService; final TimeoutPredictionService timeoutPredictionService;
final ImageCodecService imageCodecService;
final ReceivedImageStore receivedImageStore;
final ImageStreamReassembler imageReassembler;
const MeshCoreApp({ const MeshCoreApp({
super.key, super.key,
@@ -167,24 +329,83 @@ class MeshCoreApp extends StatelessWidget {
required this.translationService, required this.translationService,
required this.uiViewStateService, required this.uiViewStateService,
required this.timeoutPredictionService, 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final connector = widget.connector;
final storage = widget.storage;
return MultiProvider( return MultiProvider(
providers: [ providers: [
ChangeNotifierProvider.value(value: connector), ChangeNotifierProvider.value(value: connector),
ChangeNotifierProvider.value(value: retryService), ChangeNotifierProvider.value(value: widget.retryService),
ChangeNotifierProvider.value(value: pathHistoryService), ChangeNotifierProvider.value(value: widget.pathHistoryService),
ChangeNotifierProvider.value(value: appSettingsService), ChangeNotifierProvider.value(value: widget.appSettingsService),
ChangeNotifierProvider.value(value: bleDebugLogService), ChangeNotifierProvider.value(value: widget.bleDebugLogService),
ChangeNotifierProvider.value(value: appDebugLogService), ChangeNotifierProvider.value(value: widget.appDebugLogService),
ChangeNotifierProvider.value(value: chatTextScaleService), ChangeNotifierProvider.value(value: widget.chatTextScaleService),
ChangeNotifierProvider.value(value: translationService), ChangeNotifierProvider.value(value: widget.translationService),
ChangeNotifierProvider.value(value: uiViewStateService), ChangeNotifierProvider.value(value: widget.uiViewStateService),
Provider.value(value: storage), Provider.value(value: storage),
Provider.value(value: mapTileCacheService), ChangeNotifierProvider.value(value: widget.mapTileCacheService),
ChangeNotifierProvider.value(value: timeoutPredictionService), ChangeNotifierProvider.value(value: widget.timeoutPredictionService),
ChangeNotifierProvider.value(value: widget.imageCodecService),
ChangeNotifierProvider.value(value: widget.receivedImageStore),
], ],
child: Consumer<AppSettingsService>( child: Consumer<AppSettingsService>(
builder: (context, settingsService, child) { builder: (context, settingsService, child) {
+136
View File
@@ -1,3 +1,4 @@
import 'image_codec_support.dart';
import 'translation_support.dart'; import 'translation_support.dart';
enum UnitSystem { metric, imperial } enum UnitSystem { metric, imperial }
@@ -76,6 +77,7 @@ class Cyr2LatProfile {
class AppSettings { class AppSettings {
static const Object _unset = Object(); static const Object _unset = Object();
static const String stadiaDemo = '51bd0381-4685-4666-bae8-48940f6d77c0';
final bool clearPathOnMaxRetry; final bool clearPathOnMaxRetry;
final bool mapShowRepeaters; final bool mapShowRepeaters;
@@ -91,10 +93,15 @@ class AppSettings {
final Map<String, double>? mapCacheBounds; final Map<String, double>? mapCacheBounds;
final int mapCacheMinZoom; final int mapCacheMinZoom;
final int mapCacheMaxZoom; final int mapCacheMaxZoom;
final String mapRasterSourceId;
final String mapTileEndpointId;
final String? mapTileApiKey;
final bool notificationsEnabled; final bool notificationsEnabled;
final bool notifyOnNewMessage; final bool notifyOnNewMessage;
final bool notifyOnNewChannelMessage; final bool notifyOnNewChannelMessage;
final bool notifyOnNewAdvert; final bool notifyOnNewAdvert;
final bool autoSendZeroHopAdvertOnGpsUpdate;
final int gpsIntervalSeconds;
final bool autoRouteRotationEnabled; final bool autoRouteRotationEnabled;
final double maxRouteWeight; final double maxRouteWeight;
final double initialRouteWeight; final double initialRouteWeight;
@@ -112,6 +119,29 @@ class AppSettings {
final String tcpServerAddress; final String tcpServerAddress;
final int tcpServerPort; final int tcpServerPort;
final bool jumpToOldestUnread; 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 translationEnabled;
final bool autoTranslateIncomingMessages; final bool autoTranslateIncomingMessages;
final String? translationTargetLanguageCode; final String? translationTargetLanguageCode;
@@ -122,6 +152,26 @@ class AppSettings {
final List<Cyr2LatProfile> cyr2latProfiles; final List<Cyr2LatProfile> cyr2latProfiles;
final String selectedCyr2latProfileId; 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 { Map<String, String> get cyr2latCharMap {
final profile = cyr2latProfiles.firstWhere( final profile = cyr2latProfiles.firstWhere(
(p) => p.id == selectedCyr2latProfileId, (p) => p.id == selectedCyr2latProfileId,
@@ -145,10 +195,15 @@ class AppSettings {
this.mapCacheBounds, this.mapCacheBounds,
this.mapCacheMinZoom = 10, this.mapCacheMinZoom = 10,
this.mapCacheMaxZoom = 15, this.mapCacheMaxZoom = 15,
this.mapRasterSourceId = 'osm_auto',
this.mapTileEndpointId = 'standard_2x',
this.mapTileApiKey,
this.notificationsEnabled = true, this.notificationsEnabled = true,
this.notifyOnNewMessage = true, this.notifyOnNewMessage = true,
this.notifyOnNewChannelMessage = true, this.notifyOnNewChannelMessage = true,
this.notifyOnNewAdvert = true, this.notifyOnNewAdvert = true,
this.autoSendZeroHopAdvertOnGpsUpdate = false,
this.gpsIntervalSeconds = 900,
this.autoRouteRotationEnabled = true, this.autoRouteRotationEnabled = true,
this.maxRouteWeight = 5.0, this.maxRouteWeight = 5.0,
this.initialRouteWeight = 3.0, this.initialRouteWeight = 3.0,
@@ -166,6 +221,13 @@ class AppSettings {
this.tcpServerAddress = '', this.tcpServerAddress = '',
this.tcpServerPort = 0, this.tcpServerPort = 0,
this.jumpToOldestUnread = false, 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.translationEnabled = false,
this.autoTranslateIncomingMessages = true, this.autoTranslateIncomingMessages = true,
this.translationTargetLanguageCode, this.translationTargetLanguageCode,
@@ -178,6 +240,7 @@ class AppSettings {
}) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {}, }) : batteryChemistryByDeviceId = batteryChemistryByDeviceId ?? {},
batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {}, batteryChemistryByRepeaterId = batteryChemistryByRepeaterId ?? {},
mutedChannels = mutedChannels ?? {}, mutedChannels = mutedChannels ?? {},
imageCodecDownloadedModels = imageCodecDownloadedModels ?? const [],
translationDownloadedModels = translationDownloadedModels ?? const [], translationDownloadedModels = translationDownloadedModels ?? const [],
cyr2latProfiles = cyr2latProfiles =
cyr2latProfiles ?? cyr2latProfiles ??
@@ -206,10 +269,16 @@ class AppSettings {
'map_cache_bounds': mapCacheBounds, 'map_cache_bounds': mapCacheBounds,
'map_cache_min_zoom': mapCacheMinZoom, 'map_cache_min_zoom': mapCacheMinZoom,
'map_cache_max_zoom': mapCacheMaxZoom, 'map_cache_max_zoom': mapCacheMaxZoom,
'map_raster_source_id': mapRasterSourceId,
'map_tile_endpoint_id': mapTileEndpointId,
'map_tile_api_key': mapTileApiKey,
'notifications_enabled': notificationsEnabled, 'notifications_enabled': notificationsEnabled,
'notify_on_new_message': notifyOnNewMessage, 'notify_on_new_message': notifyOnNewMessage,
'notify_on_new_channel_message': notifyOnNewChannelMessage, 'notify_on_new_channel_message': notifyOnNewChannelMessage,
'notify_on_new_advert': notifyOnNewAdvert, 'notify_on_new_advert': notifyOnNewAdvert,
'auto_send_zero_hop_advert_on_gps_update':
autoSendZeroHopAdvertOnGpsUpdate,
'gps_interval_seconds': gpsIntervalSeconds,
'auto_route_rotation_enabled': autoRouteRotationEnabled, 'auto_route_rotation_enabled': autoRouteRotationEnabled,
'max_route_weight': maxRouteWeight, 'max_route_weight': maxRouteWeight,
'initial_route_weight': initialRouteWeight, 'initial_route_weight': initialRouteWeight,
@@ -227,6 +296,15 @@ class AppSettings {
'tcp_server_address': tcpServerAddress, 'tcp_server_address': tcpServerAddress,
'tcp_server_port': tcpServerPort, 'tcp_server_port': tcpServerPort,
'jump_to_oldest_unread': jumpToOldestUnread, '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, 'translation_enabled': translationEnabled,
'auto_translate_incoming_messages': autoTranslateIncomingMessages, 'auto_translate_incoming_messages': autoTranslateIncomingMessages,
'translation_target_language_code': translationTargetLanguageCode, 'translation_target_language_code': translationTargetLanguageCode,
@@ -270,11 +348,18 @@ class AppSettings {
), ),
mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10, mapCacheMinZoom: json['map_cache_min_zoom'] as int? ?? 10,
mapCacheMaxZoom: json['map_cache_max_zoom'] as int? ?? 15, 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, notificationsEnabled: json['notifications_enabled'] as bool? ?? true,
notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true, notifyOnNewMessage: json['notify_on_new_message'] as bool? ?? true,
notifyOnNewChannelMessage: notifyOnNewChannelMessage:
json['notify_on_new_channel_message'] as bool? ?? true, json['notify_on_new_channel_message'] as bool? ?? true,
notifyOnNewAdvert: json['notify_on_new_advert'] 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: autoRouteRotationEnabled:
json['auto_route_rotation_enabled'] as bool? ?? true, json['auto_route_rotation_enabled'] as bool? ?? true,
maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0, maxRouteWeight: (json['max_route_weight'] as num?)?.toDouble() ?? 5.0,
@@ -309,6 +394,23 @@ class AppSettings {
tcpServerAddress: json['tcp_server_address'] as String? ?? '', tcpServerAddress: json['tcp_server_address'] as String? ?? '',
tcpServerPort: json['tcp_server_port'] as int? ?? 0, tcpServerPort: json['tcp_server_port'] as int? ?? 0,
jumpToOldestUnread: json['jump_to_oldest_unread'] as bool? ?? false, 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, translationEnabled: json['translation_enabled'] as bool? ?? false,
autoTranslateIncomingMessages: autoTranslateIncomingMessages:
json['auto_translate_incoming_messages'] as bool? ?? true, json['auto_translate_incoming_messages'] as bool? ?? true,
@@ -379,10 +481,15 @@ class AppSettings {
Object? mapCacheBounds = _unset, Object? mapCacheBounds = _unset,
int? mapCacheMinZoom, int? mapCacheMinZoom,
int? mapCacheMaxZoom, int? mapCacheMaxZoom,
String? mapRasterSourceId,
String? mapTileEndpointId,
Object? mapTileApiKey = _unset,
bool? notificationsEnabled, bool? notificationsEnabled,
bool? notifyOnNewMessage, bool? notifyOnNewMessage,
bool? notifyOnNewChannelMessage, bool? notifyOnNewChannelMessage,
bool? notifyOnNewAdvert, bool? notifyOnNewAdvert,
bool? autoSendZeroHopAdvertOnGpsUpdate,
int? gpsIntervalSeconds,
bool? autoRouteRotationEnabled, bool? autoRouteRotationEnabled,
double? maxRouteWeight, double? maxRouteWeight,
double? initialRouteWeight, double? initialRouteWeight,
@@ -400,6 +507,13 @@ class AppSettings {
String? tcpServerAddress, String? tcpServerAddress,
int? tcpServerPort, int? tcpServerPort,
bool? jumpToOldestUnread, bool? jumpToOldestUnread,
bool? imageMessagesEnabled,
bool? imageProcessAutomatically,
bool? imageCodecEnabled,
Object? imageCodecSelectedModelId = _unset,
Object? imageCodecModelSourceUrl = _unset,
int? imageCodecRatePoint,
List<ImageCodecModelRecord>? imageCodecDownloadedModels,
bool? translationEnabled, bool? translationEnabled,
bool? autoTranslateIncomingMessages, bool? autoTranslateIncomingMessages,
Object? translationTargetLanguageCode = _unset, Object? translationTargetLanguageCode = _unset,
@@ -428,11 +542,20 @@ class AppSettings {
: mapCacheBounds as Map<String, double>?, : mapCacheBounds as Map<String, double>?,
mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom, mapCacheMinZoom: mapCacheMinZoom ?? this.mapCacheMinZoom,
mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom, mapCacheMaxZoom: mapCacheMaxZoom ?? this.mapCacheMaxZoom,
mapRasterSourceId: mapRasterSourceId ?? this.mapRasterSourceId,
mapTileEndpointId: mapTileEndpointId ?? this.mapTileEndpointId,
mapTileApiKey: mapTileApiKey == _unset
? this.mapTileApiKey
: mapTileApiKey as String?,
notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled, notificationsEnabled: notificationsEnabled ?? this.notificationsEnabled,
notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage, notifyOnNewMessage: notifyOnNewMessage ?? this.notifyOnNewMessage,
notifyOnNewChannelMessage: notifyOnNewChannelMessage:
notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage, notifyOnNewChannelMessage ?? this.notifyOnNewChannelMessage,
notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert, notifyOnNewAdvert: notifyOnNewAdvert ?? this.notifyOnNewAdvert,
autoSendZeroHopAdvertOnGpsUpdate:
autoSendZeroHopAdvertOnGpsUpdate ??
this.autoSendZeroHopAdvertOnGpsUpdate,
gpsIntervalSeconds: gpsIntervalSeconds ?? this.gpsIntervalSeconds,
autoRouteRotationEnabled: autoRouteRotationEnabled:
autoRouteRotationEnabled ?? this.autoRouteRotationEnabled, autoRouteRotationEnabled ?? this.autoRouteRotationEnabled,
maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight, maxRouteWeight: maxRouteWeight ?? this.maxRouteWeight,
@@ -458,6 +581,19 @@ class AppSettings {
tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress, tcpServerAddress: tcpServerAddress ?? this.tcpServerAddress,
tcpServerPort: tcpServerPort ?? this.tcpServerPort, tcpServerPort: tcpServerPort ?? this.tcpServerPort,
jumpToOldestUnread: jumpToOldestUnread ?? this.jumpToOldestUnread, 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, translationEnabled: translationEnabled ?? this.translationEnabled,
autoTranslateIncomingMessages: autoTranslateIncomingMessages:
autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages, autoTranslateIncomingMessages ?? this.autoTranslateIncomingMessages,
+4
View File
@@ -27,6 +27,10 @@ class Channel {
bool get isPublicChannel => pskHex == publicChannelPsk; bool get isPublicChannel => pskHex == publicChannelPsk;
bool get isHashtagChannel => name.startsWith('#');
bool get isPrivateChannel => !isPublicChannel && !isHashtagChannel;
static Channel? fromFrame(Uint8List frame) { static Channel? fromFrame(Uint8List frame) {
// CHANNEL_INFO format: // CHANNEL_INFO format:
// [0] = RESP_CODE_CHANNEL_INFO (18) // [0] = RESP_CODE_CHANNEL_INFO (18)
+17 -5
View File
@@ -41,6 +41,7 @@ class ChannelMessage {
final List<Repeat> repeats; final List<Repeat> repeats;
final int repeatCount; final int repeatCount;
final int? pathLength; final int? pathLength;
final int? pathHashWidth;
final Uint8List pathBytes; final Uint8List pathBytes;
final List<Uint8List> pathVariants; final List<Uint8List> pathVariants;
final int? channelIndex; final int? channelIndex;
@@ -66,6 +67,7 @@ class ChannelMessage {
this.repeats = const [], this.repeats = const [],
this.repeatCount = 0, this.repeatCount = 0,
this.pathLength, this.pathLength,
this.pathHashWidth,
Uint8List? pathBytes, Uint8List? pathBytes,
List<Uint8List>? pathVariants, List<Uint8List>? pathVariants,
this.channelIndex, this.channelIndex,
@@ -93,6 +95,7 @@ class ChannelMessage {
List<Repeat>? repeats, List<Repeat>? repeats,
int? repeatCount, int? repeatCount,
int? pathLength, int? pathLength,
int? pathHashWidth,
Uint8List? pathBytes, Uint8List? pathBytes,
List<Uint8List>? pathVariants, List<Uint8List>? pathVariants,
String? packetHash, String? packetHash,
@@ -129,6 +132,7 @@ class ChannelMessage {
repeats: repeats ?? this.repeats, repeats: repeats ?? this.repeats,
repeatCount: repeatCount ?? this.repeatCount, repeatCount: repeatCount ?? this.repeatCount,
pathLength: pathLength ?? this.pathLength, pathLength: pathLength ?? this.pathLength,
pathHashWidth: pathHashWidth ?? this.pathHashWidth,
pathBytes: pathBytes ?? this.pathBytes, pathBytes: pathBytes ?? this.pathBytes,
pathVariants: pathVariants ?? this.pathVariants, pathVariants: pathVariants ?? this.pathVariants,
channelIndex: channelIndex, channelIndex: channelIndex,
@@ -155,6 +159,7 @@ class ChannelMessage {
int pathLen; int pathLen;
int txtType; int txtType;
int? packetPathHashWidth;
Uint8List pathBytes = Uint8List(0); Uint8List pathBytes = Uint8List(0);
int channelIdx; int channelIdx;
if (code == respCodeChannelMsgRecvV3) { if (code == respCodeChannelMsgRecvV3) {
@@ -163,12 +168,18 @@ class ChannelMessage {
final hasPath = (flags & 0x01) != 0; final hasPath = (flags & 0x01) != 0;
reader.skipBytes(1); // Skip reserved byte reader.skipBytes(1); // Skip reserved byte
channelIdx = reader.readByte(); channelIdx = reader.readByte();
pathLen = reader.readInt8(); final pathByte = reader.readUInt8();
txtType = reader.readByte(); // pathByte packs: top 2 bits = hash width mode, low 6 bits = hop count
if (hasPath && pathLen > 0) { packetPathHashWidth = ((pathByte & 0xC0) >> 6) + 1;
reader.rewind(); // Rewind to read path length again for pathBytes final hopCount = pathByte & 0x3F;
pathBytes = reader.readBytes(pathLen); 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 { } else {
channelIdx = reader.readByte(); channelIdx = reader.readByte();
pathLen = reader.readInt8(); pathLen = reader.readInt8();
@@ -209,6 +220,7 @@ class ChannelMessage {
isOutgoing: false, isOutgoing: false,
status: ChannelMessageStatus.sent, status: ChannelMessageStatus.sent,
pathLength: pathLen, pathLength: pathLen,
pathHashWidth: packetPathHashWidth,
pathBytes: pathBytes, pathBytes: pathBytes,
channelIndex: channelIdx, channelIndex: channelIdx,
); );
+3 -12
View File
@@ -1,8 +1,8 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:math';
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:crypto/crypto.dart' as crypto; import 'package:crypto/crypto.dart' as crypto;
import 'package:meshcore_open/utils/keys.dart';
import 'channel.dart'; import 'channel.dart';
@@ -37,11 +37,7 @@ class Community {
/// Generate a new community with a random 32-byte secret /// Generate a new community with a random 32-byte secret
factory Community.create({required String id, required String name}) { factory Community.create({required String id, required String name}) {
final random = Random.secure(); final secret = randomBytes(32);
final secret = Uint8List(32);
for (int i = 0; i < 32; i++) {
secret[i] = random.nextInt(256);
}
return Community( return Community(
id: id, id: id,
name: name, name: name,
@@ -210,12 +206,7 @@ class Community {
/// Create a copy of this community with a regenerated random secret /// Create a copy of this community with a regenerated random secret
Community withRegeneratedSecret() { Community withRegeneratedSecret() {
final random = Random.secure(); return withNewSecret(randomBytes(32));
final newSecret = Uint8List(32);
for (int i = 0; i < 32; i++) {
newSecret[i] = random.nextInt(256);
}
return withNewSecret(newSecret);
} }
/// Extract secret from QR data (for updating existing community) /// Extract secret from QR data (for updating existing community)
+14 -4
View File
@@ -119,7 +119,7 @@ class Contact {
String pathFormattedIdList(int hashByteWidth) { String pathFormattedIdList(int hashByteWidth) {
final pathBytes = pathBytesForDisplay; final pathBytes = pathBytesForDisplay;
if (pathBytes.isEmpty) return ''; if (pathBytes.isEmpty) return '';
final w = hashByteWidth.clamp(1, 8); final w = hashByteWidth.clamp(1, 4);
final parts = <String>[]; final parts = <String>[];
for (int i = 0; i < pathBytes.length; i += w) { for (int i = 0; i < pathBytes.length; i += w) {
final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length; final end = (i + w) <= pathBytes.length ? (i + w) : pathBytes.length;
@@ -166,8 +166,18 @@ class Contact {
final type = reader.readByte(); final type = reader.readByte();
final flags = reader.readByte(); final flags = reader.readByte();
final pathLen = reader.readByte(); final pathLen = reader.readByte();
final safePathLen = pathLen > 0 int hopCount = 0;
? (pathLen > maxPathSize ? maxPathSize : pathLen) 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; : 0;
final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen); final pathBytes = reader.readBytes(maxPathSize).sublist(0, safePathLen);
final name = reader.readCStringGreedy(maxNameSize); final name = reader.readCStringGreedy(maxNameSize);
@@ -213,7 +223,7 @@ class Contact {
name: name.isEmpty ? 'Unknown' : name, name: name.isEmpty ? 'Unknown' : name,
type: type, type: type,
flags: flags, flags: flags,
pathLength: (pathLen == 0xFF || pathLen > maxPathSize) ? -1 : pathLen, pathLength: hopCount,
path: pathBytes, path: pathBytes,
latitude: lat, latitude: lat,
longitude: lon, longitude: lon,
+3 -2
View File
@@ -1,3 +1,4 @@
import 'dart:typed_data';
import 'dart:ui'; import 'dart:ui';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
@@ -13,8 +14,8 @@ class DisplayPath {
final Color color; final Color color;
final bool isPrimary; final bool isPrimary;
/// Outbound hop bytes, including hops that could not be placed on the map. /// Outbound hop prefixes, including hops that could not be placed on the map.
final List<int> hopBytes; final List<Uint8List> hopBytes;
/// Resolved map points: self, each locatable hop, then the target when its /// Resolved map points: self, each locatable hop, then the target when its
/// position is known. Hops with no position are skipped here but still /// position is known. Hops with no position are skipped here but still
+835
View File
@@ -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,
);
}
}
+12 -2
View File
@@ -81,6 +81,16 @@ class RadioSettings {
txPowerDbm: 20, txPowerDbm: 20,
), ),
), ),
(
'Australia (Mid)',
RadioSettings(
frequencyMHz: 915.075,
bandwidth: LoRaBandwidth.bw125,
spreadingFactor: LoRaSpreadingFactor.sf9,
codingRate: LoRaCodingRate.cr4_5,
txPowerDbm: 20,
),
),
( (
'Australia SA, WA, QLD', 'Australia SA, WA, QLD',
RadioSettings( RadioSettings(
@@ -466,8 +476,8 @@ class RadioSettings {
RadioSettings( RadioSettings(
frequencyMHz: 908.205, frequencyMHz: 908.205,
bandwidth: LoRaBandwidth.bw62_5, bandwidth: LoRaBandwidth.bw62_5,
spreadingFactor: LoRaSpreadingFactor.sf10, spreadingFactor: LoRaSpreadingFactor.sf9,
codingRate: LoRaCodingRate.cr4_5, codingRate: LoRaCodingRate.cr4_8,
txPowerDbm: 20, txPowerDbm: 20,
), ),
), ),
+664 -92
View File
@@ -6,8 +6,11 @@ import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/image_codec_support.dart';
import '../models/translation_support.dart'; import '../models/translation_support.dart';
import '../services/app_settings_service.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/notification_service.dart';
import '../services/translation_service.dart'; import '../services/translation_service.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
@@ -18,7 +21,16 @@ import '../helpers/snack_bar_builder.dart';
import 'map_cache_screen.dart'; import 'map_cache_screen.dart';
class AppSettingsScreen extends StatelessWidget { 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -597,10 +609,206 @@ class AppSettingsScreen extends StatelessWidget {
settingsService.setEnableMessageTracing(value); 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( Widget _buildBatteryContent(
BuildContext context, BuildContext context,
AppSettingsService settingsService, AppSettingsService settingsService,
@@ -687,6 +895,10 @@ class AppSettingsScreen extends StatelessWidget {
value: 'lipo', value: 'lipo',
child: Text(context.l10n.appSettings_batteryLipo), 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 scheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme; final textTheme = Theme.of(context).textTheme;
return Column( final children = <Widget>[
children: [ SwitchListTile(
SwitchListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
contentPadding: const EdgeInsets.symmetric( secondary: const Icon(Icons.router_outlined, size: 20),
horizontal: 16, title: Text(context.l10n.appSettings_showRepeaters),
vertical: 4, 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( const Divider(height: 1, indent: 16),
contentPadding: const EdgeInsets.symmetric( InkWell(
horizontal: 16, onTap: () => _showUnitsSheet(context, settingsService),
vertical: 4, 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( const Divider(height: 1, indent: 16),
contentPadding: const EdgeInsets.symmetric( InkWell(
horizontal: 16, onTap: () {
vertical: 4, 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), const Divider(height: 1, indent: 16),
InkWell( InkWell(
onTap: () => _showTimeFilterSheet(context, settingsService), onTap: () => _showMapRasterEndpointDialog(context, settingsService),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
Icons.timer_outlined, Icons.public_outlined,
size: 20, size: 20,
color: scheme.onSurfaceVariant, color: scheme.onSurfaceVariant,
), ),
@@ -754,19 +1143,14 @@ class AppSettingsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
context.l10n.appSettings_timeFilter, context.l10n.appSettings_stadiaEndpoint,
style: textTheme.bodyMedium?.copyWith( style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
settingsService.settings.mapTimeFilterHours == 0 _mapRasterEndpointSummary(settingsService.settings),
? context.l10n.appSettings_timeFilterShowAll
: context.l10n.appSettings_timeFilterShowLast(
settingsService.settings.mapTimeFilterHours
.toInt(),
),
style: textTheme.bodySmall?.copyWith( style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant, color: scheme.onSurfaceVariant,
), ),
@@ -785,13 +1169,13 @@ class AppSettingsScreen extends StatelessWidget {
), ),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
InkWell( InkWell(
onTap: () => _showUnitsSheet(context, settingsService), onTap: () => _showMapApiKeyDialog(context, settingsService),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Row( child: Row(
children: [ children: [
Icon( Icon(
Icons.straighten, Icons.key_outlined,
size: 20, size: 20,
color: scheme.onSurfaceVariant, color: scheme.onSurfaceVariant,
), ),
@@ -801,17 +1185,14 @@ class AppSettingsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text( Text(
context.l10n.appSettings_unitsTitle, context.l10n.appSettings_stadiaApiKey,
style: textTheme.bodyMedium?.copyWith( style: textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
settingsService.settings.unitSystem == _mapApiKeySummary(context, settingsService.settings),
UnitSystem.imperial
? context.l10n.appSettings_unitsImperial
: context.l10n.appSettings_unitsMetric,
style: textTheme.bodySmall?.copyWith( style: textTheme.bodySmall?.copyWith(
color: scheme.onSurfaceVariant, color: scheme.onSurfaceVariant,
), ),
@@ -828,59 +1209,208 @@ class AppSettingsScreen extends StatelessWidget {
), ),
), ),
), ),
const Divider(height: 1, indent: 16), ]);
InkWell( }
onTap: () {
Navigator.push( return Column(children: children);
context, }
MaterialPageRoute(builder: (context) => const MapCacheScreen()),
); String _mapRasterSourceSummary(AppSettings settings) {
}, final source = MapRasterSourceCatalog.fromSettings(settings);
child: Padding( return '${source.label} - ${source.description}';
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), }
child: Row(
children: [ bool _isStadiaSource(AppSettings settings) {
Icon( return MapRasterSourceCatalog.fromSettings(settings).isStadia;
Icons.download_outlined, }
size: 20,
color: scheme.onSurfaceVariant, String _mapRasterEndpointSummary(AppSettings settings) {
), final endpoint = MapRasterEndpointCatalog.fromSettings(settings);
const SizedBox(width: 12), return '${endpoint.label} - ${endpoint.description}';
Expanded( }
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( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( for (final preset in MapRasterSourcePreset.values)
context.l10n.appSettings_offlineMapCache, Builder(
style: textTheme.bodyMedium?.copyWith( builder: (context) {
fontWeight: FontWeight.w600, 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;
}
+37 -17
View File
@@ -10,6 +10,13 @@ import '../helpers/snack_bar_builder.dart';
enum _BleLogView { frames, rawLogRx } 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 { class BleDebugLogScreen extends StatefulWidget {
const BleDebugLogScreen({super.key}); const BleDebugLogScreen({super.key});
@@ -110,20 +117,32 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
final entry = entries[index]; final entry = entries[index];
final time = final time =
'${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}'; '${entry.timestamp.hour.toString().padLeft(2, '0')}:${entry.timestamp.minute.toString().padLeft(2, '0')}:${entry.timestamp.second.toString().padLeft(2, '0')}';
return GestureDetector( Future<void> copyHex() async {
onLongPress: () async { await Clipboard.setData(
await Clipboard.setData( ClipboardData(
ClipboardData( text: entry.payload
text: entry.payload .map(
.map( (b) => b
(b) => b .toRadixString(16)
.toRadixString(16) .padLeft(2, '0'),
.padLeft(2, '0'), )
) .join(''),
.join(''), ),
);
if (context.mounted) {
showDismissibleSnackBar(
context,
content: Text(
context.l10n.debugLog_bleCopied,
), ),
); );
}, }
}
return GestureDetector(
onTap: copyHex,
onLongPress: copyHex,
onSecondaryTap: copyHex,
child: Container( child: Container(
color: MeshPalette.bg, color: MeshPalette.bg,
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
@@ -302,16 +321,17 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
rawHex: _bytesToHex(raw), rawHex: _bytesToHex(raw),
); );
} }
final pathLen = raw[index++]; final pathLenRaw = raw[index++];
if (raw.length < index + pathLen) { final pathByteLen = _decodeRawPathByteLen(pathLenRaw);
if (raw.length < index + pathByteLen) {
return _RawPacketInfo( return _RawPacketInfo(
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}', title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
summary: 'Truncated path', summary: 'Truncated path',
rawHex: _bytesToHex(raw), rawHex: _bytesToHex(raw),
); );
} }
final pathBytes = raw.sublist(index, index + pathLen); final pathBytes = raw.sublist(index, index + pathByteLen);
index += pathLen; index += pathByteLen;
if (raw.length <= index) { if (raw.length <= index) {
return _RawPacketInfo( return _RawPacketInfo(
title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}', title: 'RX RAW_LOG_RX_DATA • ${_payloadTypeLabel(payloadType)}',
@@ -324,7 +344,7 @@ class _BleDebugLogScreenState extends State<BleDebugLogScreen> {
final title = final title =
'RX ${_payloadTypeLabel(payloadType)}${_routeLabel(routeType)} • v$payloadVer'; 'RX ${_payloadTypeLabel(payloadType)}${_routeLabel(routeType)} • v$payloadVer';
final summary = _decodePayloadSummary(payloadType, payload); final summary = _decodePayloadSummary(payloadType, payload);
final pathSummary = pathLen > 0 final pathSummary = pathByteLen > 0
? 'Path=${_bytesToHex(pathBytes)}' ? 'Path=${_bytesToHex(pathBytes)}'
: 'Path=none'; : 'Path=none';
final detail = '$summary$pathSummary • len=${raw.length}'; final detail = '$summary$pathSummary • len=${raw.length}';
File diff suppressed because it is too large Load Diff
+135 -52
View File
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/helpers/path_helper.dart';
import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -22,7 +23,6 @@ import '../theme/mesh_theme.dart';
import '../widgets/adaptive_app_bar_title.dart'; import '../widgets/adaptive_app_bar_title.dart';
import '../widgets/mesh_ui.dart'; import '../widgets/mesh_ui.dart';
import '../widgets/path_map_ui.dart'; import '../widgets/path_map_ui.dart';
import '../widgets/themed_map_tile_layer.dart';
class ChannelMessagePathScreen extends StatelessWidget { class ChannelMessagePathScreen extends StatelessWidget {
final ChannelMessage message; final ChannelMessage message;
@@ -43,19 +43,40 @@ class ChannelMessagePathScreen extends StatelessWidget {
message.pathVariants, message.pathVariants,
); );
final primaryPath = !channelMessage && !message.isOutgoing final hashByteWidth =
? Uint8List.fromList(primaryPathTmp.reversed.toList()) (message.pathHashWidth ?? connector.pathHashByteWidth)
: primaryPathTmp; .clamp(1, 4)
.toInt();
final primaryPath = _orientPathBytes(
primaryPathTmp,
hashByteWidth,
reverse: !channelMessage && !message.isOutgoing,
);
final hops = _buildPathHops( final hops = _buildPathHops(
primaryPath, primaryPath,
connector, connector,
l10n, l10n,
hashByteWidth,
resolveFromEnd: !message.isOutgoing, resolveFromEnd: !message.isOutgoing,
); );
final hasHopDetails = primaryPath.isNotEmpty; 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, primaryPath.length,
message.pathLength, hashByteWidth,
);
final reportedHopCount = message.pathLength;
final effectiveHopCount = observedHopCount > 0
? observedHopCount
: reportedHopCount;
final observedLabel = _formatObservedHops(
observedHopCount,
effectiveHopCount,
l10n, l10n,
); );
final extraPaths = _otherPaths(primaryPath, message.pathVariants); final extraPaths = _otherPaths(primaryPath, message.pathVariants);
@@ -73,11 +94,8 @@ class ChannelMessagePathScreen extends StatelessWidget {
title: context.l10n.contacts_repeaterPathTrace, title: context.l10n.contacts_repeaterPathTrace,
path: primaryPath, path: primaryPath,
flipPathAround: true, flipPathAround: true,
reversePathAround: reversePathAround: false,
!(!channelMessage && !message.isOutgoing), pathHashByteWidth: hashByteWidth,
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
), ),
), ),
), ),
@@ -98,13 +116,20 @@ class ChannelMessagePathScreen extends StatelessWidget {
child: ListView( child: ListView(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
children: [ children: [
_buildSummaryCard(context, observedLabel: observedLabel), _buildSummaryCard(
context,
observedLabel: observedLabel,
effectiveHopCount: effectiveHopCount,
),
const SizedBox(height: 16),
if (extraPaths.isNotEmpty) ...[ if (extraPaths.isNotEmpty) ...[
SectionHeader( SectionHeader(
l10n.channelPath_otherObservedPaths, l10n.channelPath_otherObservedPaths,
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
), ),
_buildPathVariants(context, extraPaths), const SizedBox(height: 8),
_buildPathVariants(context, extraPaths, hashByteWidth),
const SizedBox(height: 16),
], ],
SectionHeader( SectionHeader(
l10n.channelPath_repeaterHops, 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 l10n = context.l10n;
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final routeChip = message.pathLength == null final routeChip = effectiveHopCount == null
? null ? null
: message.pathLength! < 0 : effectiveHopCount < 0
? const RouteChip(isDirect: false) ? const RouteChip(isDirect: false)
: RouteChip(isDirect: true, hops: message.pathLength); : RouteChip(isDirect: true, hops: effectiveHopCount);
return MeshCard( return MeshCard(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
@@ -172,7 +201,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
_buildDetailRow( _buildDetailRow(
context, context,
l10n.channelPath_pathLabelTitle, l10n.channelPath_pathLabelTitle,
_formatPathLabel(message.pathLength, l10n), _formatPathLabel(effectiveHopCount, l10n),
scheme: scheme, scheme: scheme,
), ),
if (observedLabel != null) 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; final l10n = context.l10n;
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -210,7 +243,11 @@ class ChannelMessagePathScreen extends StatelessWidget {
Text( Text(
l10n.channelPath_observedPathTitle( l10n.channelPath_observedPathTitle(
i + 1, i + 1,
_formatHopCount(variants[i].length, l10n), _formatHopCount(
variants[i].length,
hashByteWidth,
l10n,
),
), ),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@@ -218,7 +255,7 @@ class ChannelMessagePathScreen extends StatelessWidget {
), ),
const SizedBox(height: 2), const SizedBox(height: 2),
Text( Text(
_formatPathPrefixes(variants[i]), _formatPathPrefixes(variants[i], hashByteWidth),
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 11, fontSize: 11,
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
@@ -402,31 +439,31 @@ class ChannelMessagePathScreen extends StatelessWidget {
); );
} }
String _formatPathLabel(int? pathLength, AppLocalizations l10n) { String _formatPathLabel(int? hopCount, AppLocalizations l10n) {
if (pathLength == null) return l10n.channelPath_unknownPath; if (hopCount == null) return l10n.channelPath_unknownPath;
if (pathLength < 0) return l10n.channelPath_floodPath; if (hopCount < 0) return l10n.channelPath_floodPath;
if (pathLength == 0) return l10n.channelPath_directPath; if (hopCount == 0) return l10n.channelPath_directPath;
return l10n.chat_hopsCount(pathLength); return l10n.chat_hopsCount(hopCount);
} }
String? _formatObservedHops( String? _formatObservedHops(
int observedCount, int observedCount,
int? pathLength, int? targetHopCount,
AppLocalizations l10n, AppLocalizations l10n,
) { ) {
if (observedCount <= 0 && (pathLength == null || pathLength <= 0)) { if (observedCount <= 0 && (targetHopCount == null || targetHopCount <= 0)) {
return null; return null;
} }
if (pathLength == null || pathLength < 0) { if (targetHopCount == null || targetHopCount < 0) {
return observedCount > 0 ? l10n.chat_hopsCount(observedCount) : null; return observedCount > 0 ? l10n.chat_hopsCount(observedCount) : null;
} }
if (observedCount == 0) { 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.chat_hopsCount(observedCount);
} }
return l10n.channelPath_observedSomeOf(observedCount, pathLength); return l10n.channelPath_observedSomeOf(observedCount, targetHopCount);
} }
Widget _buildDetailRow( Widget _buildDetailRow(
@@ -623,7 +660,10 @@ class _ChannelMessagePathMapScreenState
? kPrimaryPathColor ? kPrimaryPathColor
: kAlternatePathColors[(index - 1) % kAlternatePathColors.length], : kAlternatePathColors[(index - 1) % kAlternatePathColors.length],
isPrimary: isPrimary, isPrimary: isPrimary,
hopBytes: List<int>.from(orientedBytes), hopBytes: [
for (final hop in hops)
if (hop.hopBytes != null) Uint8List.fromList(hop.hopBytes!),
],
points: points, points: points,
pointLabels: labels, pointLabels: labels,
pointConfirmed: confirmed, pointConfirmed: confirmed,
@@ -808,7 +848,11 @@ class _ChannelMessagePathMapScreenState
primaryPath, 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 // Match on the unoriented bytes observedPaths stores them as
// recorded, while selectedPath may be reversed for display. // recorded, while selectedPath may be reversed for display.
@@ -817,19 +861,21 @@ class _ChannelMessagePathMapScreenState
selectedPath, selectedPath,
connector, connector,
context.l10n, context.l10n,
width,
resolveFromEnd: !widget.message.isOutgoing, resolveFromEnd: !widget.message.isOutgoing,
); );
// Renderable paths for the animation and combined view. // Renderable paths for the animation and combined view.
final entries = <_ObservedPathEntry>[]; final entries = <_ObservedPathEntry>[];
for (var i = 0; i < observedPaths.length; i++) { 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 final pathHops = i == selectedIndex
? hops ? hops
: _buildPathHops( : _buildPathHops(
oriented, oriented,
connector, connector,
context.l10n, context.l10n,
width,
resolveFromEnd: !widget.message.isOutgoing, resolveFromEnd: !widget.message.isOutgoing,
); );
final display = _buildDisplayPath( final display = _buildDisplayPath(
@@ -908,7 +954,7 @@ class _ChannelMessagePathMapScreenState
? LatLngBounds.fromPoints(points) ? LatLngBounds.fromPoints(points)
: null; : null;
final mapKey = ValueKey( final mapKey = ValueKey(
'${_formatPathPrefixes(selectedPath)},${context.l10n.pathTrace_you}', '${_formatPathPrefixes(selectedPath, width)},${context.l10n.pathTrace_you}',
); );
_pathDistance = _getPathDistance(points); _pathDistance = _getPathDistance(points);
@@ -967,7 +1013,7 @@ class _ChannelMessagePathMapScreenState
}, },
), ),
children: [ children: [
ThemedMapTileLayer(tileCache: tileCache), tileCache.buildTileLayer(context),
AnimatedBuilder( AnimatedBuilder(
animation: _playback, animation: _playback,
builder: (context, _) { builder: (context, _) {
@@ -1045,6 +1091,7 @@ class _ChannelMessagePathMapScreenState
context, context,
observedPaths, observedPaths,
selectedIndex, selectedIndex,
width,
(index) { (index) {
setState(() { setState(() {
_selectedPath = observedPaths[index].pathBytes; _selectedPath = observedPaths[index].pathBytes;
@@ -1086,10 +1133,12 @@ class _ChannelMessagePathMapScreenState
BuildContext context, BuildContext context,
List<_ObservedPath> paths, List<_ObservedPath> paths,
int selectedIndex, int selectedIndex,
int hashByteWidth,
ValueChanged<int> onSelected, { ValueChanged<int> onSelected, {
double topOffset = 16, double topOffset = 16,
}) { }) {
final l10n = context.l10n; final l10n = context.l10n;
final width = hashByteWidth.clamp(1, 4).toInt();
final selectedPath = paths[selectedIndex]; final selectedPath = paths[selectedIndex];
final label = selectedPath.isPrimary final label = selectedPath.isPrimary
? l10n.channelPath_primaryPath(selectedIndex + 1) ? l10n.channelPath_primaryPath(selectedIndex + 1)
@@ -1120,7 +1169,7 @@ class _ChannelMessagePathMapScreenState
value: i, value: i,
child: Text( child: Text(
'${paths[i].isPrimary ? l10n.channelPath_primaryPath(i + 1) : l10n.channelPath_pathLabel(i + 1)}' '${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( Text(
l10n.channelPath_selectedPathLabel( l10n.channelPath_selectedPathLabel(
label, label,
_formatPathPrefixes(selectedPath.pathBytes), _formatPathPrefixes(selectedPath.pathBytes, width),
), ),
style: TextStyle( style: TextStyle(
color: Theme.of(context).colorScheme.onSurfaceVariant, color: Theme.of(context).colorScheme.onSurfaceVariant,
@@ -1378,11 +1427,11 @@ class _ChannelMessagePathMapScreenState
} }
/// Orients recorded path bytes in the direction the packet traveled. /// Orients recorded path bytes in the direction the packet traveled.
Uint8List _orientPath(Uint8List bytes) { Uint8List _orientPath(Uint8List bytes, int hashByteWidth) {
final reverse = final reverse =
(!widget.message.isOutgoing && !widget.channelMessage) || (!widget.message.isOutgoing && !widget.channelMessage) ||
(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}) { Marker _buildNodeLabelMarker({required LatLng point, required String label}) {
@@ -1738,6 +1787,7 @@ class _PathHop {
final Contact? contact; final Contact? contact;
final LatLng? position; final LatLng? position;
final AppLocalizations l10n; final AppLocalizations l10n;
final Uint8List? hopBytes;
const _PathHop({ const _PathHop({
required this.index, required this.index,
@@ -1745,12 +1795,18 @@ class _PathHop {
required this.contact, required this.contact,
required this.position, required this.position,
required this.l10n, required this.l10n,
this.hopBytes,
}); });
bool get hasLocation => position != null; bool get hasLocation => position != null;
String get displayLabel { 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)}'; return '($prefixLabel) ${_resolveName(contact, l10n)}';
} }
} }
@@ -1765,10 +1821,12 @@ class _ObservedPath {
List<_PathHop> _buildPathHops( List<_PathHop> _buildPathHops(
Uint8List pathBytes, Uint8List pathBytes,
MeshCoreConnector connector, MeshCoreConnector connector,
AppLocalizations l10n, { AppLocalizations l10n,
int hashByteWidth, {
bool resolveFromEnd = false, bool resolveFromEnd = false,
}) { }) {
if (pathBytes.isEmpty) return const []; if (pathBytes.isEmpty) return const [];
final width = hashByteWidth.clamp(1, 4).toInt();
final endpoint = final endpoint =
(connector.selfLatitude != null && connector.selfLongitude != null) (connector.selfLatitude != null && connector.selfLongitude != null)
? LatLng(connector.selfLatitude!, connector.selfLongitude!) ? LatLng(connector.selfLatitude!, connector.selfLongitude!)
@@ -1778,19 +1836,23 @@ List<_PathHop> _buildPathHops(
contacts: connector.allContacts, contacts: connector.allContacts,
endpoint: endpoint, endpoint: endpoint,
resolveFromEnd: resolveFromEnd, resolveFromEnd: resolveFromEnd,
pathHashByteWidth: width,
); );
final hopChunks = PathHelper.splitPathBytes(pathBytes, width);
final hops = <_PathHop>[]; final hops = <_PathHop>[];
for (var i = 0; i < pathBytes.length; i++) { for (var i = 0; i < hopChunks.length; i++) {
final contact = resolvedContacts[i]; final hopBytes = hopChunks[i];
final contact = i < resolvedContacts.length ? resolvedContacts[i] : null;
final resolvedPosition = _resolvePosition(contact); final resolvedPosition = _resolvePosition(contact);
hops.add( hops.add(
_PathHop( _PathHop(
index: i + 1, index: i + 1,
prefix: pathBytes[i], prefix: hopBytes.isNotEmpty ? hopBytes[0] : 0,
contact: contact, contact: contact,
position: resolvedPosition, position: resolvedPosition,
l10n: l10n, l10n: l10n,
hopBytes: hopBytes,
), ),
); );
} }
@@ -1810,14 +1872,35 @@ String _formatPrefix(int prefix) {
return prefix.toRadixString(16).padLeft(2, '0').toUpperCase(); return prefix.toRadixString(16).padLeft(2, '0').toUpperCase();
} }
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes, int hashByteWidth) {
return pathBytes return PathHelper.splitPathBytes(
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) pathBytes,
.join(','); hashByteWidth,
).map(PathHelper.formatHopHex).join(',');
} }
String _formatHopCount(int count, AppLocalizations l10n) { Uint8List _orientPathBytes(
return l10n.chat_hopsCount(count); 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) { String _resolveName(Contact? contact, AppLocalizations l10n) {
+161 -134
View File
@@ -1,9 +1,9 @@
import 'dart:async'; import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meshcore_open/storage/channel_message_store.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/utils/platform_info.dart';
import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
@@ -27,6 +27,7 @@ import '../widgets/qr_code_display.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../widgets/sync_progress_overlay.dart'; import '../widgets/sync_progress_overlay.dart';
import '../widgets/unread_badge.dart'; import '../widgets/unread_badge.dart';
import '../helpers/gif_helper.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import 'channel_chat_screen.dart'; import 'channel_chat_screen.dart';
import 'community_qr_scanner_screen.dart'; import 'community_qr_scanner_screen.dart';
@@ -282,8 +283,6 @@ class _ChannelsScreenState extends State<ChannelsScreen>
buildDefaultDragHandles: false, buildDefaultDragHandles: false,
itemCount: filteredChannels.length, itemCount: filteredChannels.length,
onReorderItem: (oldIndex, newIndex) { onReorderItem: (oldIndex, newIndex) {
// onReorderItem already adjusts newIndex after the
// removed item, unlike the deprecated onReorder.
final reordered = List<Channel>.from( final reordered = List<Channel>.from(
filteredChannels, filteredChannels,
); );
@@ -374,13 +373,30 @@ class _ChannelsScreenState extends State<ChannelsScreen>
_communityIndex, _communityIndex,
); );
final bool isCommunityChannel = Channel.isCommunityChannel(channelType); 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) { switch (channelType) {
case ChannelType.communityPublic: case ChannelType.communityPublic:
icon = Icons.groups; icon = Icons.groups;
iconColor = MeshPalette.magenta; iconColor = MeshPalette.magenta;
if (community != null) {
subtitle =
'${context.l10n.community_publicChannel}${community.name}';
}
case ChannelType.communityHashtag: case ChannelType.communityHashtag:
icon = Icons.groups; icon = Icons.groups;
iconColor = MeshPalette.magenta; iconColor = MeshPalette.magenta;
if (community != null) {
subtitle =
'${context.l10n.community_hashtagChannel}${community.name}';
}
case ChannelType.public: case ChannelType.public:
icon = Icons.public; icon = Icons.public;
iconColor = MeshPalette.signal; iconColor = MeshPalette.signal;
@@ -395,7 +411,12 @@ class _ChannelsScreenState extends State<ChannelsScreen>
// Last message preview // Last message preview
final messages = connector.getChannelMessages(channel); final messages = connector.getChannelMessages(channel);
final lastMessage = messages.isNotEmpty ? messages.last : null; 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 lastTime = lastMessage?.timestamp;
final channelLabel = channel.name.isEmpty final channelLabel = channel.name.isEmpty
@@ -429,142 +450,156 @@ class _ChannelsScreenState extends State<ChannelsScreen>
channelMessageStore, channelMessageStore,
channel, channel,
), ),
child: GestureDetector( onSecondaryTap: PlatformInfo.isDesktop
onSecondaryTapUp: PlatformInfo.isDesktop ? () => _showChannelActions(
? (_) => _showChannelActions( this.context,
this.context, connector,
connector, channelMessageStore,
channelMessageStore, channel,
channel, )
) : null,
: null, child: Row(
child: Row( crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center, children: [
children: [ // Leading avatar with optional community badge
// Leading avatar with optional community badge Stack(
Stack( clipBehavior: Clip.none,
clipBehavior: Clip.none, children: [
children: [ AvatarCircle(
AvatarCircle( name: channelLabel,
name: channelLabel, size: 42,
size: 42, color: iconColor,
color: iconColor, icon: icon,
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,
),
],
],
), ),
), if (isCommunityChannel)
const SizedBox(width: 8), Positioned(
// Right side: time + unread badge + muted + drag handle right: -2,
Column( bottom: -2,
crossAxisAlignment: CrossAxisAlignment.end, 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, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (lastTime != null)
Text(
_relativeTime(lastTime),
style: MeshTheme.mono(
fontSize: 11,
color: scheme.onSurfaceVariant,
),
),
const SizedBox(height: 4),
Row( Row(
mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
if (isMuted) ...[ Expanded(
Icon( child: Text(
Icons.notifications_off, channelLabel,
size: 14, style: Theme.of(context).textTheme.bodyMedium
color: scheme.onSurfaceVariant, ?.copyWith(fontWeight: FontWeight.w500),
maxLines: 1,
overflow: TextOverflow.ellipsis,
), ),
const SizedBox(width: 4), ),
], const SizedBox(width: 6),
if (unreadCount > 0) UnreadBadge(count: unreadCount), 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), const SizedBox(width: 8),
ReorderableDragStartListener( // Right side: time + unread badge + muted + drag handle
index: dragIndex, Column(
child: Padding( crossAxisAlignment: CrossAxisAlignment.end,
padding: const EdgeInsets.all(8), mainAxisSize: MainAxisSize.min,
child: Icon( children: [
Icons.drag_handle, if (lastTime != null)
Text(
_relativeTime(lastTime),
style: MeshTheme.mono(
fontSize: 11,
color: scheme.onSurfaceVariant, 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; return;
} }
final random = Random.secure(); final psk = randomBytes(16);
final psk = Uint8List(16);
for (int i = 0; i < 16; i++) {
psk[i] = random.nextInt(256);
}
Navigator.pop(sheetContext); Navigator.pop(sheetContext);
await connector.setChannel( await connector.setChannel(
nextIndex, nextIndex,
@@ -1545,11 +1576,7 @@ class _ChannelsScreenState extends State<ChannelsScreen>
icon: const Icon(Icons.casino), icon: const Icon(Icons.casino),
tooltip: sheetContext.l10n.channels_generateRandomPsk, tooltip: sheetContext.l10n.channels_generateRandomPsk,
onPressed: () { onPressed: () {
final random = Random.secure(); final bytes = randomBytes(16);
final bytes = Uint8List(16);
for (int i = 0; i < 16; i++) {
bytes[i] = random.nextInt(256);
}
pskController.text = Channel.formatPskHex(bytes); pskController.text = Channel.formatPskHex(bytes);
}, },
), ),
+42 -6
View File
@@ -12,6 +12,7 @@ import '../utils/platform_info.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../helpers/cyr2lat.dart'; import '../helpers/cyr2lat.dart';
import '../helpers/path_helper.dart';
import '../helpers/reaction_helper.dart'; import '../helpers/reaction_helper.dart';
import '../widgets/message_status_icon.dart'; import '../widgets/message_status_icon.dart';
import '../widgets/empty_state.dart'; import '../widgets/empty_state.dart';
@@ -46,6 +47,14 @@ import '../theme/mesh_theme.dart';
import '../widgets/mesh_ui.dart'; import '../widgets/mesh_ui.dart';
import 'telemetry_screen.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 { class ChatScreen extends StatefulWidget {
final Contact contact; final Contact contact;
final int initialUnreadCount; final int initialUnreadCount;
@@ -479,6 +488,9 @@ class _ChatScreenState extends State<ChatScreen> {
languageCode: settings.translationTargetLanguageCode, languageCode: settings.translationTargetLanguageCode,
onPressed: _showTranslationOptions, 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( Expanded(
child: ValueListenableBuilder<TextEditingValue>( child: ValueListenableBuilder<TextEditingValue>(
valueListenable: _textController, valueListenable: _textController,
@@ -546,15 +558,15 @@ class _ChatScreenState extends State<ChatScreen> {
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.chat_typeMessage, hintText: context.l10n.chat_typeMessage,
border: OutlineInputBorder( border: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill), borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide(color: scheme.outlineVariant), borderSide: BorderSide(color: scheme.outlineVariant),
), ),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill), borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide(color: scheme.outlineVariant), borderSide: BorderSide(color: scheme.outlineVariant),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(MeshRadii.pill), borderRadius: BorderRadius.circular(MeshRadii.md),
borderSide: BorderSide( borderSide: BorderSide(
color: scheme.primary, color: scheme.primary,
width: 1.5, width: 1.5,
@@ -776,17 +788,35 @@ class _ChatScreenState extends State<ChatScreen> {
} }
String _currentPathLabel(Contact contact) { String _currentPathLabel(Contact contact) {
final connector = context.read<MeshCoreConnector>();
// Check if user has set a path override // Check if user has set a path override
if (contact.pathOverride != null) { if (contact.pathOverride != null) {
if (contact.pathOverride! < 0) return context.l10n.chat_floodForced; if (contact.pathOverride! < 0) return context.l10n.chat_floodForced;
if (contact.pathOverride == 0) return context.l10n.chat_directForced; 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 // Use device's path
if (contact.pathLength < 0) return context.l10n.chat_floodAuto; if (contact.pathLength < 0) return context.l10n.chat_floodAuto;
if (contact.pathLength == 0) return context.l10n.chat_direct; 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) { void _showContactInfo(BuildContext context) {
@@ -807,7 +837,10 @@ class _ChatScreenState extends State<ChatScreen> {
), ),
_buildInfoRow( _buildInfoRow(
context.l10n.chat_path, context.l10n.chat_path,
contact.pathLabel(context.l10n), contact.pathLabel(
context.l10n,
pathHashByteWidth: connector.pathHashByteWidth,
),
), ),
_buildInfoRow( _buildInfoRow(
context.l10n.contact_lastSeen, context.l10n.contact_lastSeen,
@@ -1435,6 +1468,9 @@ class _MessageBubble extends StatelessWidget {
color: textColor.withValues(alpha: 0.72), color: textColor.withValues(alpha: 0.72),
fontSize: bodyFontSize * textScale, fontSize: bodyFontSize * textScale,
), ),
onSecondaryTap: PlatformInfo.isDesktop
? onLongPress
: null,
), ),
), ),
], ],
+26 -4
View File
@@ -915,6 +915,7 @@ class _ContactsScreenState extends State<ContactsScreen>
return _ContactTileEntrance( return _ContactTileEntrance(
index: index, index: index,
contact: contact, contact: contact,
pathHashByteWidth: connector.pathHashByteWidth,
lastSeen: _resolveLastSeen(contact), lastSeen: _resolveLastSeen(contact),
unreadCount: unreadCount, unreadCount: unreadCount,
isFavorite: contact.isFavorite, isFavorite: contact.isFavorite,
@@ -1371,7 +1372,10 @@ class _ContactsScreenState extends State<ContactsScreen>
MaterialPageRoute( MaterialPageRoute(
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: context.l10n.contacts_repeaterPing, title: context.l10n.contacts_repeaterPing,
path: Uint8List.fromList([contact.publicKey.first]), path: contact.pathBytesForDisplay.isNotEmpty
? contact.pathBytesForDisplay
: _contactPathPrefix(contact, hw),
flipPathAround: true,
targetContact: contact, targetContact: contact,
pathHashByteWidth: hw, pathHashByteWidth: hw,
), ),
@@ -1405,8 +1409,8 @@ class _ContactsScreenState extends State<ContactsScreen>
: context.l10n.contacts_roomPing, : context.l10n.contacts_roomPing,
path: contact.pathBytesForDisplay.isNotEmpty path: contact.pathBytesForDisplay.isNotEmpty
? contact.pathBytesForDisplay ? contact.pathBytesForDisplay
: Uint8List.fromList([contact.publicKey.first]), : _contactPathPrefix(contact, hw),
flipPathAround: contact.pathBytesForDisplay.isNotEmpty, flipPathAround: true,
targetContact: contact, targetContact: contact,
pathHashByteWidth: hw, 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( void _confirmDelete(
BuildContext context, BuildContext context,
MeshCoreConnector connector, MeshCoreConnector connector,
@@ -1549,6 +1563,7 @@ class _ContactsScreenState extends State<ContactsScreen>
class _ContactTile extends StatelessWidget { class _ContactTile extends StatelessWidget {
final Contact contact; final Contact contact;
final int pathHashByteWidth;
final DateTime lastSeen; final DateTime lastSeen;
final int unreadCount; final int unreadCount;
final bool isFavorite; final bool isFavorite;
@@ -1557,6 +1572,7 @@ class _ContactTile extends StatelessWidget {
const _ContactTile({ const _ContactTile({
required this.contact, required this.contact,
required this.pathHashByteWidth,
required this.lastSeen, required this.lastSeen,
required this.unreadCount, required this.unreadCount,
required this.isFavorite, required this.isFavorite,
@@ -1676,7 +1692,10 @@ class _ContactTile extends StatelessWidget {
children: [ children: [
Expanded( Expanded(
child: Text( child: Text(
contact.pathLabel(context.l10n), contact.pathLabel(
context.l10n,
pathHashByteWidth: pathHashByteWidth,
),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
style: TextStyle( style: TextStyle(
@@ -1763,6 +1782,7 @@ class _ContactTile extends StatelessWidget {
class _ContactTileEntrance extends StatelessWidget { class _ContactTileEntrance extends StatelessWidget {
final int index; final int index;
final Contact contact; final Contact contact;
final int pathHashByteWidth;
final DateTime lastSeen; final DateTime lastSeen;
final int unreadCount; final int unreadCount;
final bool isFavorite; final bool isFavorite;
@@ -1772,6 +1792,7 @@ class _ContactTileEntrance extends StatelessWidget {
const _ContactTileEntrance({ const _ContactTileEntrance({
required this.index, required this.index,
required this.contact, required this.contact,
required this.pathHashByteWidth,
required this.lastSeen, required this.lastSeen,
required this.unreadCount, required this.unreadCount,
required this.isFavorite, required this.isFavorite,
@@ -1785,6 +1806,7 @@ class _ContactTileEntrance extends StatelessWidget {
index: index, index: index,
child: _ContactTile( child: _ContactTile(
contact: contact, contact: contact,
pathHashByteWidth: pathHashByteWidth,
lastSeen: lastSeen, lastSeen: lastSeen,
unreadCount: unreadCount, unreadCount: unreadCount,
isFavorite: isFavorite, isFavorite: isFavorite,
+3 -7
View File
@@ -147,13 +147,6 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
connector, connector,
index, index,
); );
if (PlatformInfo.isDesktop) {
return GestureDetector(
onSecondaryTapUp: (_) =>
_showContactContextMenu(contact, connector),
child: tile,
);
}
return tile; return tile;
}, },
), ),
@@ -204,6 +197,9 @@ class _DiscoveryScreenState extends State<DiscoveryScreen> {
} }
}, },
onLongPress: () => _showContactContextMenu(contact, connector), onLongPress: () => _showContactContextMenu(contact, connector),
onSecondaryTap: PlatformInfo.isDesktop
? () => _showContactContextMenu(contact, connector)
: null,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
child: Row( child: Row(
children: [ children: [
+4 -4
View File
@@ -20,7 +20,6 @@ import '../widgets/app_bar.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../icons/los_icon.dart'; import '../icons/los_icon.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
import '../widgets/themed_map_tile_layer.dart';
class LineOfSightEndpoint { class LineOfSightEndpoint {
final String label; final String label;
@@ -357,7 +356,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final settings = context.watch<AppSettingsService>().settings; final settings = context.watch<AppSettingsService>().settings;
final isImperial = settings.unitSystem == UnitSystem.imperial; final isImperial = settings.unitSystem == UnitSystem.imperial;
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.watch<MapTileCacheService>();
final endpoints = _visibleEndpoints(); final endpoints = _visibleEndpoints();
final mapPoints = [ final mapPoints = [
if (_start != null) _start!.point, if (_start != null) _start!.point,
@@ -430,6 +429,7 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
minZoom: _mapMinZoom, minZoom: _mapMinZoom,
maxZoom: _mapMaxZoom, maxZoom: _mapMaxZoom,
onLongPress: (_, point) => _addCustomPoint(point), onLongPress: (_, point) => _addCustomPoint(point),
onSecondaryTap: (_, point) => _addCustomPoint(point),
onPositionChanged: (camera, hasGesture) { onPositionChanged: (camera, hasGesture) {
final shouldShow = camera.zoom >= _labelZoomThreshold; final shouldShow = camera.zoom >= _labelZoomThreshold;
if (!_didReceivePositionUpdate || if (!_didReceivePositionUpdate ||
@@ -442,8 +442,8 @@ class _LineOfSightMapScreenState extends State<LineOfSightMapScreen> {
}, },
), ),
children: [ children: [
ThemedMapTileLayer( tileCache.buildTileLayer(
tileCache: tileCache, context,
opacity: _showTerrainLayer ? 1 : 0.72, opacity: _showTerrainLayer ? 1 : 0.72,
), ),
if (_result != null && _result!.segments.isNotEmpty) if (_result != null && _result!.segments.isNotEmpty)
+130 -6
View File
@@ -12,7 +12,6 @@ import '../widgets/adaptive_app_bar_title.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
import '../widgets/mesh_ui.dart'; import '../widgets/mesh_ui.dart';
import '../widgets/themed_map_tile_layer.dart';
class MapCacheScreen extends StatefulWidget { class MapCacheScreen extends StatefulWidget {
const MapCacheScreen({super.key}); const MapCacheScreen({super.key});
@@ -34,6 +33,11 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
bool _isDownloading = false; bool _isDownloading = false;
int _completedTiles = 0; int _completedTiles = 0;
int _failedTiles = 0; int _failedTiles = 0;
List<CachedTileInfo> _cachedTiles = const [];
int _cachedTileBytes = 0;
bool _isLoadingCachedTiles = false;
double _overlayZoom = 2.0;
LatLngBounds? _visibleBounds;
@override @override
void initState() { void initState() {
@@ -123,8 +127,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
_minZoom = safeMin; _minZoom = safeMin;
_maxZoom = safeMax; _maxZoom = safeMax;
_selectedBounds = bounds; _selectedBounds = bounds;
_visibleBounds = bounds;
}); });
_updateEstimate(); _updateEstimate();
_refreshCachedTiles();
if (bounds != null) { if (bounds != null) {
_mapController.fitCamera( _mapController.fitCamera(
CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)), CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(48)),
@@ -181,6 +187,7 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
Future<void> _startDownload() async { Future<void> _startDownload() async {
final bounds = _selectedBounds; final bounds = _selectedBounds;
final cacheService = context.read<MapTileCacheService>();
if (bounds == null) { if (bounds == null) {
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
@@ -197,6 +204,18 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
return; return;
} }
if (!cacheService.source.allowsBulkDownload) {
showDismissibleSnackBar(
context,
content: Text(
context.l10n.mapCache_bulkDownloadDisabledInConfig(
cacheService.source.label,
),
),
);
return;
}
final confirmed = await showDialog<bool>( final confirmed = await showDialog<bool>(
context: context, context: context,
builder: (dialogContext) => AlertDialog( builder: (dialogContext) => AlertDialog(
@@ -219,8 +238,6 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
if (confirmed != true || !mounted) return; if (confirmed != true || !mounted) return;
final cacheService = context.read<MapTileCacheService>();
setState(() { setState(() {
_isDownloading = true; _isDownloading = true;
_completedTiles = 0; _completedTiles = 0;
@@ -254,6 +271,8 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
result.failed, result.failed,
) )
: context.l10n.mapCache_cachedTiles(result.downloaded); : context.l10n.mapCache_cachedTiles(result.downloaded);
await _refreshCachedTiles();
if (!mounted) return;
showDismissibleSnackBar(context, content: Text(message)); showDismissibleSnackBar(context, content: Text(message));
} }
@@ -280,16 +299,61 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
final cacheService = context.read<MapTileCacheService>(); final cacheService = context.read<MapTileCacheService>();
await cacheService.clearCache(); await cacheService.clearCache();
if (!mounted) return; if (!mounted) return;
await _refreshCachedTiles();
if (!mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
context, context,
content: Text(context.l10n.mapCache_offlineCacheCleared), content: Text(context.l10n.mapCache_offlineCacheCleared),
); );
} }
Future<void> _refreshCachedTiles() async {
if (!mounted) return;
setState(() {
_isLoadingCachedTiles = true;
});
final cacheService = context.read<MapTileCacheService>();
final inventory = await cacheService.getCachedTileInventory();
if (!mounted) return;
setState(() {
_cachedTiles = inventory.tiles;
_cachedTileBytes = inventory.totalBytes;
_isLoadingCachedTiles = false;
});
}
String _formatBytes(int bytes) {
if (bytes < 1024) return '$bytes B';
if (bytes < 1024 * 1024) {
return '${(bytes / 1024).toStringAsFixed(1)} KB';
}
return '${(bytes / (1024 * 1024)).toStringAsFixed(1)} MB';
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.watch<MapTileCacheService>();
final source = tileCache.source;
final selectedBounds = _selectedBounds; final selectedBounds = _selectedBounds;
final activeCachedTiles = tileCache.filterTilesForActiveSource(
_cachedTiles,
);
final cachedInSelection = tileCache.countTilesForBounds(
activeCachedTiles,
bounds: selectedBounds,
minZoom: _minZoom,
maxZoom: _maxZoom,
);
final overlayPolygons = tileCache.buildCachedTilePolygons(
activeCachedTiles,
zoom: _overlayZoom.round().clamp(0, 19).toInt(),
visibleBounds: _visibleBounds,
);
final cacheSummary =
'${context.l10n.mapCache_summarySource(source.label)}\n'
'${context.l10n.mapCache_summaryCachedTilesForSource(activeCachedTiles.length)}\n'
'${context.l10n.mapCache_summaryCachedInSelection(cachedInSelection)}\n'
'${context.l10n.mapCache_summaryApproxCacheSize(_formatBytes(_cachedTileBytes))}';
final l10n = context.l10n; final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
@@ -327,9 +391,23 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
) )
: const KeyboardOptions.disabled(), : const KeyboardOptions.disabled(),
), ),
onPositionChanged: (camera, hasGesture) {
final nextZoom = camera.zoom;
final nextBounds = camera.visibleBounds;
if (_overlayZoom != nextZoom ||
_visibleBounds?.north != nextBounds.north ||
_visibleBounds?.south != nextBounds.south ||
_visibleBounds?.east != nextBounds.east ||
_visibleBounds?.west != nextBounds.west) {
setState(() {
_overlayZoom = nextZoom;
_visibleBounds = nextBounds;
});
}
},
), ),
children: [ children: [
ThemedMapTileLayer(tileCache: tileCache), tileCache.buildTileLayer(context),
if (selectedBounds != null) if (selectedBounds != null)
PolygonLayer( PolygonLayer(
polygons: [ polygons: [
@@ -341,9 +419,24 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
), ),
], ],
), ),
if (overlayPolygons.isNotEmpty)
PolygonLayer(polygons: overlayPolygons),
], ],
), ),
if (isDesktop) _buildDesktopMapControls(), if (isDesktop) _buildDesktopMapControls(),
Positioned(
top: 12,
left: isDesktop ? 84 : 12,
child: Card(
child: Padding(
padding: const EdgeInsets.all(8),
child: Text(
'Z: ${_overlayZoom.round()}:',
style: const TextStyle(fontSize: 12),
),
),
),
),
Positioned( Positioned(
top: 12, top: 12,
right: 12, right: 12,
@@ -445,6 +538,34 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
color: scheme.onSurfaceVariant, color: scheme.onSurfaceVariant,
), ),
), ),
const SizedBox(height: 12),
TextFormField(
key: ValueKey(
'$cacheSummary|${activeCachedTiles.length}|$cachedInSelection|$_cachedTileBytes',
),
initialValue: cacheSummary,
readOnly: true,
minLines: 4,
maxLines: 4,
decoration: InputDecoration(
labelText: _isLoadingCachedTiles
? l10n.mapCache_cachedTilesLabel
: l10n.mapCache_cachedTileSummaryLabel,
border: const OutlineInputBorder(),
),
),
if (!source.allowsBulkDownload) ...[
const SizedBox(height: 8),
Text(
l10n.mapCache_bulkDownloadDisabledForSource(
source.label,
),
style: MeshTheme.mono(
fontSize: 12,
color: MeshPalette.alert,
),
),
],
if (_isDownloading) ...[ if (_isDownloading) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
LinearProgressIndicator( LinearProgressIndicator(
@@ -471,7 +592,10 @@ class _MapCacheScreenState extends State<MapCacheScreen> {
child: ElevatedButton.icon( child: ElevatedButton.icon(
icon: const Icon(Icons.download), icon: const Icon(Icons.download),
label: Text(l10n.mapCache_downloadTilesButton), label: Text(l10n.mapCache_downloadTilesButton),
onPressed: _isDownloading || selectedBounds == null onPressed:
_isDownloading ||
selectedBounds == null ||
!source.allowsBulkDownload
? null ? null
: _startDownload, : _startDownload,
), ),
+264 -152
View File
@@ -5,13 +5,14 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/helpers/path_helper.dart';
import 'package:meshcore_open/screens/path_trace_map.dart'; import 'package:meshcore_open/screens/path_trace_map.dart';
import 'package:meshcore_open/widgets/app_bar.dart'; import 'package:meshcore_open/widgets/app_bar.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../connector/meshcore_connector.dart'; import '../connector/meshcore_connector.dart';
import '../l10n/l10n.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart';
import '../models/app_settings.dart'; import '../models/app_settings.dart';
import '../models/channel.dart'; import '../models/channel.dart';
import '../models/contact.dart'; import '../models/contact.dart';
@@ -25,7 +26,6 @@ import '../utils/battery_utils.dart';
import '../utils/route_transitions.dart'; import '../utils/route_transitions.dart';
import '../widgets/quick_switch_bar.dart'; import '../widgets/quick_switch_bar.dart';
import '../widgets/sync_progress_overlay.dart'; import '../widgets/sync_progress_overlay.dart';
import '../widgets/themed_map_tile_layer.dart';
import '../icons/los_icon.dart'; import '../icons/los_icon.dart';
import 'channels_screen.dart'; import 'channels_screen.dart';
import 'chat_screen.dart'; import 'chat_screen.dart';
@@ -79,6 +79,7 @@ class _MapScreenState extends State<MapScreen> {
bool _hasInitializedMap = false; bool _hasInitializedMap = false;
bool _removedMarkersLoaded = false; bool _removedMarkersLoaded = false;
final List<int> _pathTrace = []; final List<int> _pathTrace = [];
final List<int> _pathTraceHopWidths = [];
final List<Contact> _pathTraceContacts = []; final List<Contact> _pathTraceContacts = [];
final List<LatLng> _points = []; final List<LatLng> _points = [];
final List<Polyline> _polylines = []; final List<Polyline> _polylines = [];
@@ -107,6 +108,31 @@ class _MapScreenState extends State<MapScreen> {
super.dispose(); super.dispose();
} }
ColorScheme get _overlayScheme => Theme.of(context).colorScheme;
bool get _useDarkOverlay => Theme.of(context).brightness == Brightness.dark;
Color get _overlayPanelColor => _useDarkOverlay
? MapPalette.panelDark
: _overlayScheme.surfaceContainerLow.withValues(alpha: 0.96);
Color get _overlayPrimaryTextColor =>
_useDarkOverlay ? MapPalette.textPrimary : _overlayScheme.onSurface;
Color get _overlaySecondaryTextColor => _useDarkOverlay
? MapPalette.textSecondary
: _overlayScheme.onSurfaceVariant;
Color get _overlayMutedTextColor =>
_useDarkOverlay ? MapPalette.textMuted : _overlayScheme.onSurfaceVariant;
Color get _overlayBorderColor =>
_useDarkOverlay ? MapPalette.border : _overlayScheme.outlineVariant;
Color get _overlayShadowColor => _useDarkOverlay
? MapPalette.markerShadow
: Colors.black.withValues(alpha: 0.18);
_NodeAge _ageOf(Contact contact) { _NodeAge _ageOf(Contact contact) {
final d = DateTime.now().difference(contact.lastSeen); final d = DateTime.now().difference(contact.lastSeen);
if (d.inMinutes <= 60) return _NodeAge.online; if (d.inMinutes <= 60) return _NodeAge.online;
@@ -233,12 +259,12 @@ class _MapScreenState extends State<MapScreen> {
bottom: 96, bottom: 96,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: MapPalette.panelDark, color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md), borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border), border: Border.all(color: _overlayBorderColor),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 8, blurRadius: 8,
offset: Offset(0, 3), offset: Offset(0, 3),
), ),
@@ -250,20 +276,20 @@ class _MapScreenState extends State<MapScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
IconButton( IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.add), icon: const Icon(Icons.add),
visualDensity: VisualDensity.standard, visualDensity: VisualDensity.standard,
tooltip: context.l10n.map_zoomIn, tooltip: context.l10n.map_zoomIn,
onPressed: () => _zoomMapBy(1), onPressed: () => _zoomMapBy(1),
), ),
IconButton( IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.remove), icon: const Icon(Icons.remove),
tooltip: context.l10n.map_zoomOut, tooltip: context.l10n.map_zoomOut,
onPressed: () => _zoomMapBy(-1), onPressed: () => _zoomMapBy(-1),
), ),
IconButton( IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.crop_free), icon: const Icon(Icons.crop_free),
tooltip: context.l10n.map_centerMap, tooltip: context.l10n.map_centerMap,
onPressed: () => _mapController.move(center, zoom), onPressed: () => _mapController.move(center, zoom),
@@ -285,6 +311,31 @@ class _MapScreenState extends State<MapScreen> {
); );
} }
void _handleMapContextPress(
BuildContext context,
MeshCoreConnector connector,
LatLng latLng,
) {
if (_isSelectingPoi) {
setState(() {
_isSelectingPoi = false;
});
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
return;
}
_showShareMarkerAtPositionSheet(
context: context,
connector: connector,
position: latLng,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Builder( return Builder(
@@ -303,6 +354,7 @@ class _MapScreenState extends State<MapScreen> {
final settingsService = context.read<AppSettingsService>(); final settingsService = context.read<AppSettingsService>();
final pathHistory = context.read<PathHistoryService>(); final pathHistory = context.read<PathHistoryService>();
final tileCache = context.read<MapTileCacheService>(); final tileCache = context.read<MapTileCacheService>();
final scheme = Theme.of(context).colorScheme;
final isDesktop = _isDesktopPlatform(defaultTargetPlatform); final isDesktop = _isDesktopPlatform(defaultTargetPlatform);
final allContacts = connector.allContacts; final allContacts = connector.allContacts;
@@ -376,17 +428,20 @@ class _MapScreenState extends State<MapScreen> {
// Compute guessed locations with caching // Compute guessed locations with caching
final maxRangeKm = _estimateLoRaRangeKm(connector); final maxRangeKm = _estimateLoRaRangeKm(connector);
final pathHashByteWidth = connector.pathHashByteWidth
.clamp(1, 4)
.toInt();
final filteredKeys = guessCandidates final filteredKeys = guessCandidates
.map((c) => '${c.publicKeyHex}:${c.path.join("-")}') .map((c) => '${c.publicKeyHex}:${c.path.join("-")}')
.join(','); .join(',');
final anchorKeys = allContactsWithLocation final anchorKeys = allContactsWithLocation
.map( .map(
(c) => (c) =>
'${c.publicKeyHex}:${c.latitude}:${c.longitude}:${c.path.isNotEmpty ? c.path.last : ""}', '${c.publicKeyHex}:${c.latitude}:${c.longitude}:${PathHelper.formatHopHex(c.path.isNotEmpty ? c.path.sublist(max(0, c.path.length - pathHashByteWidth)) : const [])}',
) )
.join(','); .join(',');
final cacheKey = final cacheKey =
'$filteredKeys|$anchorKeys|$pathHistoryVersion:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}'; '$filteredKeys|$anchorKeys|$pathHistoryVersion:$pathHashByteWidth:${connector.currentFreqHz}:${connector.currentSf}:${connector.currentBwHz}:${connector.currentTxPower}:${settings.mapShowGuessedLocations}';
if (cacheKey != _guessedLocationsCacheKey) { if (cacheKey != _guessedLocationsCacheKey) {
_guessedLocationsCacheKey = cacheKey; _guessedLocationsCacheKey = cacheKey;
_cachedGuessedLocations = settings.mapShowGuessedLocations _cachedGuessedLocations = settings.mapShowGuessedLocations
@@ -395,6 +450,7 @@ class _MapScreenState extends State<MapScreen> {
allContactsWithLocation, allContactsWithLocation,
pathHistory, pathHistory,
maxRangeKm, maxRangeKm,
pathHashByteWidth,
) )
: []; : [];
} }
@@ -554,8 +610,8 @@ class _MapScreenState extends State<MapScreen> {
canPop: allowBack, canPop: allowBack,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
backgroundColor: MapPalette.panelDark, backgroundColor: scheme.surface,
foregroundColor: MapPalette.textPrimary, foregroundColor: scheme.onSurface,
title: AppBarTitle(context.l10n.map_title), title: AppBarTitle(context.l10n.map_title),
centerTitle: true, centerTitle: true,
automaticallyImplyLeading: false, automaticallyImplyLeading: false,
@@ -708,24 +764,10 @@ class _MapScreenState extends State<MapScreen> {
} }
}, },
onLongPress: (_, latLng) { onLongPress: (_, latLng) {
if (_isSelectingPoi) { _handleMapContextPress(context, connector, latLng);
setState(() { },
_isSelectingPoi = false; onSecondaryTap: (_, latLng) {
}); _handleMapContextPress(context, connector, latLng);
_shareMarker(
context: context,
connector: connector,
position: latLng,
defaultLabel: context.l10n.map_pointOfInterest,
flags: 'poi',
);
return;
}
_showShareMarkerAtPositionSheet(
context: context,
connector: connector,
position: latLng,
);
}, },
onPositionChanged: (camera, hasGesture) { onPositionChanged: (camera, hasGesture) {
// Track zoom in half-step buckets so cluster/marker // Track zoom in half-step buckets so cluster/marker
@@ -742,7 +784,7 @@ class _MapScreenState extends State<MapScreen> {
}, },
), ),
children: [ children: [
ThemedMapTileLayer(tileCache: tileCache), tileCache.buildTileLayer(context),
if (_polylines.isNotEmpty && _isBuildingPathTrace) if (_polylines.isNotEmpty && _isBuildingPathTrace)
PolylineLayer(polylines: _polylines), PolylineLayer(polylines: _polylines),
if (sharedMarkerPolylines.isNotEmpty) if (sharedMarkerPolylines.isNotEmpty)
@@ -895,7 +937,7 @@ class _MapScreenState extends State<MapScreen> {
_handleQuickSwitch(index, context), _handleQuickSwitch(index, context),
contactsUnreadCount: connector.getTotalContactsUnreadCount(), contactsUnreadCount: connector.getTotalContactsUnreadCount(),
channelsUnreadCount: connector.getTotalChannelsUnreadCount(), channelsUnreadCount: connector.getTotalChannelsUnreadCount(),
highContrast: true, highContrast: _useDarkOverlay,
), ),
), ),
floatingActionButton: floatingActionButton:
@@ -970,23 +1012,19 @@ class _MapScreenState extends State<MapScreen> {
List<Contact> withLocation, List<Contact> withLocation,
PathHistoryService pathHistory, PathHistoryService pathHistory,
double? maxRangeKm, double? maxRangeKm,
int pathHashByteWidth,
) { ) {
// Index known-location repeaters by their 1-byte hash.
// null value = two repeaters share the same hash byte (ambiguous collision).
final repeaterByHash = <int, Contact?>{};
for (final c in withLocation) {
if (c.type == advTypeRepeater) {
if (repeaterByHash.containsKey(c.publicKey[0])) {
repeaterByHash[c.publicKey[0]] =
null; // collision: can't disambiguate
} else {
repeaterByHash[c.publicKey[0]] = c;
}
}
}
final result = <_GuessedLocation>[]; final result = <_GuessedLocation>[];
final hopWidth = pathHashByteWidth.clamp(1, 4).toInt();
final anchorsByPrefix = <String, List<Contact>>{};
for (final repeater in withLocation) {
if (repeater.type != advTypeRepeater) continue;
if (repeater.publicKey.length < hopWidth) continue;
final prefix = PathHelper.formatHopHex(
repeater.publicKey.sublist(0, hopWidth),
);
anchorsByPrefix.putIfAbsent(prefix, () => []).add(repeater);
}
for (final contact in allContacts) { for (final contact in allContacts) {
if (contact.hasLocation) continue; if (contact.hasLocation) continue;
@@ -1000,21 +1038,23 @@ class _MapScreenState extends State<MapScreen> {
// Collect the contact-side (last-hop) repeater from every known path. // Collect the contact-side (last-hop) repeater from every known path.
// path = [device-side hop, ..., contact-side hop] // path = [device-side hop, ..., contact-side hop]
// Only path.last is actually within radio range of the contact using // Only the last hop chunk is actually within radio range of the contact.
// earlier bytes would anchor against our own side of the network.
final pathSets = <List<int>>[ final pathSets = <List<int>>[
contact.path.toList(), contact.path.toList(),
...pathHistory ...pathHistory
.getRecentPaths(contact.publicKeyHex) .getRecentPaths(contact.publicKeyHex)
.map((r) => r.pathBytes), .map((r) => r.pathBytes),
]; ];
final lastHopBytes = <int>{};
for (final pathBytes in pathSets) { for (final pathBytes in pathSets) {
if (pathBytes.isEmpty) continue; if (pathBytes.isEmpty) continue;
final lastHop = pathBytes.last; final lastHop = pathBytes.sublist(max(0, pathBytes.length - hopWidth));
lastHopBytes.add(lastHop); if (lastHop.isEmpty) continue;
final r = repeaterByHash[lastHop];
if (r != null) anchorSet.add(LatLng(r.latitude!, r.longitude!)); final repeaters = anchorsByPrefix[PathHelper.formatHopHex(lastHop)];
if (repeaters != null && repeaters.isNotEmpty) {
final repeater = repeaters.first;
anchorSet.add(LatLng(repeater.latitude!, repeater.longitude!));
}
} }
// Filter anchors that are geometrically inconsistent with radio range. // Filter anchors that are geometrically inconsistent with radio range.
@@ -1181,9 +1221,12 @@ class _MapScreenState extends State<MapScreen> {
width: 48, width: 48,
height: 48, height: 48,
child: GestureDetector( child: GestureDetector(
onLongPress: () => _isBuildingPathTrace onLongPress: () {
? _showNodeInfo(context, guess.contact) if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
: null, },
onSecondaryTap: () {
if (_isBuildingPathTrace) _showNodeInfo(context, guess.contact);
},
onTap: () => _isBuildingPathTrace onTap: () => _isBuildingPathTrace
? _addToPath(context, guess.contact, position: guess.position) ? _addToPath(context, guess.contact, position: guess.position)
: _selectNode(guess.contact, guessedPosition: guess.position), : _selectNode(guess.contact, guessedPosition: guess.position),
@@ -1193,14 +1236,14 @@ class _MapScreenState extends State<MapScreen> {
height: 36, height: 36,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: MapPalette.panelDark, color: _overlayPanelColor,
border: Border.all( border: Border.all(
color: guess.highConfidence ? color : MapPalette.textMuted, color: guess.highConfidence ? color : _overlayMutedTextColor,
width: guess.highConfidence ? 2.5 : 2, width: guess.highConfidence ? 2.5 : 2,
), ),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 7, blurRadius: 7,
offset: Offset(0, 2), offset: Offset(0, 2),
), ),
@@ -1209,7 +1252,7 @@ class _MapScreenState extends State<MapScreen> {
alignment: Alignment.center, alignment: Alignment.center,
child: Icon( child: Icon(
Icons.not_listed_location, Icons.not_listed_location,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
size: 19, size: 19,
), ),
), ),
@@ -1286,12 +1329,20 @@ class _MapScreenState extends State<MapScreen> {
// Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms // Key-prefix overlaps are a visual highlight only: flag the repeaters/rooms
// whose first key byte collides with another repeater/room on the map. // whose first key byte collides with another repeater/room on the map.
final overlapPrefixes = <int>{}; final overlapPrefixes = <String>{};
if (overlapsMode) { if (overlapsMode) {
final counts = <int, int>{}; final hopWidth = context
.read<MeshCoreConnector>()
.pathHashByteWidth
.clamp(1, pubKeySize)
.toInt();
final counts = <String, int>{};
for (final contact in contacts) { for (final contact in contacts) {
if (contact.type == advTypeRepeater || contact.type == advTypeRoom) { if ((contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
final prefix = contact.publicKey.first; contact.publicKey.length >= hopWidth) {
final prefix = PathHelper.formatHopHex(
contact.publicKey.sublist(0, hopWidth),
);
counts[prefix] = (counts[prefix] ?? 0) + 1; counts[prefix] = (counts[prefix] ?? 0) + 1;
} }
} }
@@ -1299,10 +1350,20 @@ class _MapScreenState extends State<MapScreen> {
if (count > 1) overlapPrefixes.add(prefix); if (count > 1) overlapPrefixes.add(prefix);
}); });
} }
final overlapHopWidth = context
.read<MeshCoreConnector>()
.pathHashByteWidth
.clamp(1, pubKeySize)
.toInt();
bool isOverlap(Contact contact) => bool isOverlap(Contact contact) =>
overlapsMode && overlapsMode &&
(contact.type == advTypeRepeater || contact.type == advTypeRoom) && (contact.type == advTypeRepeater || contact.type == advTypeRoom) &&
overlapPrefixes.contains(contact.publicKey.first); contact.publicKey.length >= overlapHopWidth &&
overlapPrefixes.contains(
PathHelper.formatHopHex(
contact.publicKey.sublist(0, overlapHopWidth),
),
);
void addNode(Contact contact, {bool dot = false}) { void addNode(Contact contact, {bool dot = false}) {
final overlap = isOverlap(contact); final overlap = isOverlap(contact);
@@ -1383,8 +1444,12 @@ class _MapScreenState extends State<MapScreen> {
height: size, height: size,
child: GestureDetector( child: GestureDetector(
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
onLongPress: () => onLongPress: () {
_isBuildingPathTrace ? _showNodeInfo(context, contact) : null, if (_isBuildingPathTrace) _showNodeInfo(context, contact);
},
onSecondaryTap: () {
if (_isBuildingPathTrace) _showNodeInfo(context, contact);
},
onTap: () => _isBuildingPathTrace onTap: () => _isBuildingPathTrace
? _addToPath(context, contact) ? _addToPath(context, contact)
: _selectNode(contact), : _selectNode(contact),
@@ -1523,12 +1588,12 @@ class _MapScreenState extends State<MapScreen> {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration( decoration: BoxDecoration(
color: MapPalette.panelDark, color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.xs), borderRadius: BorderRadius.circular(MeshRadii.xs),
border: Border.all(color: MapPalette.border), border: Border.all(color: _overlayBorderColor),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 4, blurRadius: 4,
offset: Offset(0, 1), offset: Offset(0, 1),
), ),
@@ -1542,7 +1607,7 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 10, fontSize: 10,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
), ),
), ),
@@ -1663,7 +1728,7 @@ class _MapScreenState extends State<MapScreen> {
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle, shape: BoxShape.circle,
color: statusColor, color: statusColor,
border: Border.all(color: MapPalette.panelDark, width: 2), border: Border.all(color: _overlayPanelColor, width: 2),
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: batteryLow child: batteryLow
@@ -1686,10 +1751,10 @@ class _MapScreenState extends State<MapScreen> {
Expanded( Expanded(
child: Text( child: Text(
label, label,
style: const TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -1747,9 +1812,9 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
Expanded( Expanded(
child: Material( child: Material(
color: MapPalette.panelDark, color: _overlayPanelColor,
shape: StadiumBorder( shape: StadiumBorder(
side: const BorderSide(color: MapPalette.border), side: BorderSide(color: _overlayBorderColor),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: TextField( child: TextField(
@@ -1757,17 +1822,15 @@ class _MapScreenState extends State<MapScreen> {
focusNode: _searchFocus, focusNode: _searchFocus,
decoration: InputDecoration( decoration: InputDecoration(
hintText: context.l10n.map_searchHint, hintText: context.l10n.map_searchHint,
hintStyle: const TextStyle( hintStyle: TextStyle(color: _overlaySecondaryTextColor),
color: MapPalette.textSecondary, prefixIcon: Icon(
),
prefixIcon: const Icon(
Icons.search, Icons.search,
size: 20, size: 20,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
suffixIcon: hasQuery suffixIcon: hasQuery
? IconButton( ? IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.close, size: 18), icon: const Icon(Icons.close, size: 18),
onPressed: () { onPressed: () {
setState(() { setState(() {
@@ -1787,9 +1850,9 @@ class _MapScreenState extends State<MapScreen> {
vertical: 12, vertical: 12,
), ),
), ),
style: const TextStyle( style: TextStyle(
fontSize: 14, fontSize: 14,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
cursorColor: MapPalette.selected, cursorColor: MapPalette.selected,
@@ -1801,9 +1864,9 @@ class _MapScreenState extends State<MapScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Material( Material(
color: MapPalette.panelDark, color: _overlayPanelColor,
shape: StadiumBorder( shape: StadiumBorder(
side: const BorderSide(color: MapPalette.border), side: BorderSide(color: _overlayBorderColor),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: InkWell( child: InkWell(
@@ -1827,17 +1890,17 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
), ),
const SizedBox(width: 2), const SizedBox(width: 2),
AnimatedRotation( AnimatedRotation(
turns: _statsExpanded ? 0.5 : 0, turns: _statsExpanded ? 0.5 : 0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
child: const Icon( child: Icon(
Icons.expand_more, Icons.expand_more,
size: 16, size: 16,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
), ),
], ],
@@ -1936,12 +1999,12 @@ class _MapScreenState extends State<MapScreen> {
color: selected color: selected
? Color.alphaBlend( ? Color.alphaBlend(
accent.withValues(alpha: 0.34), accent.withValues(alpha: 0.34),
MapPalette.panelDark, _overlayPanelColor,
) )
: MapPalette.panelDark, : _overlayPanelColor,
shape: StadiumBorder( shape: StadiumBorder(
side: BorderSide( side: BorderSide(
color: selected ? accent : MapPalette.border, color: selected ? accent : _overlayBorderColor,
width: selected ? 1.5 : 1, width: selected ? 1.5 : 1,
), ),
), ),
@@ -1957,11 +2020,7 @@ class _MapScreenState extends State<MapScreen> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (selected) ...[ if (selected) ...[
const Icon( Icon(Icons.check, size: 13, color: _overlayPrimaryTextColor),
Icons.check,
size: 13,
color: MapPalette.textPrimary,
),
const SizedBox(width: 4), const SizedBox(width: 4),
], ],
Text( Text(
@@ -1970,8 +2029,8 @@ class _MapScreenState extends State<MapScreen> {
fontSize: 12.5, fontSize: 12.5,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: selected color: selected
? MapPalette.textPrimary ? _overlayPrimaryTextColor
: MapPalette.textSecondary, : _overlaySecondaryTextColor,
), ),
), ),
], ],
@@ -2001,14 +2060,14 @@ class _MapScreenState extends State<MapScreen> {
margin: const EdgeInsets.only(top: 6), margin: const EdgeInsets.only(top: 6),
constraints: const BoxConstraints(maxHeight: 300), constraints: const BoxConstraints(maxHeight: 300),
decoration: BoxDecoration( decoration: BoxDecoration(
color: MapPalette.panelDark, color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md), borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border), border: Border.all(color: _overlayBorderColor),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 10, blurRadius: 10,
offset: Offset(0, 4), offset: const Offset(0, 4),
), ),
], ],
), ),
@@ -2017,8 +2076,8 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: Text( child: Text(
context.l10n.map_noResults, context.l10n.map_noResults,
style: const TextStyle( style: TextStyle(
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
fontSize: 13, fontSize: 13,
), ),
), ),
@@ -2028,7 +2087,7 @@ class _MapScreenState extends State<MapScreen> {
padding: const EdgeInsets.symmetric(vertical: 4), padding: const EdgeInsets.symmetric(vertical: 4),
itemCount: results.length, itemCount: results.length,
separatorBuilder: (_, _) => separatorBuilder: (_, _) =>
const Divider(height: 1, color: MapPalette.border), Divider(height: 1, color: _overlayBorderColor),
itemBuilder: (context, index) { itemBuilder: (context, index) {
final c = results[index]; final c = results[index];
final color = _getNodeColor(c.type); final color = _getNodeColor(c.type);
@@ -2049,10 +2108,10 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
Text( Text(
c.name, c.name,
style: const TextStyle( style: TextStyle(
fontSize: 13.5, fontSize: 13.5,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -2060,7 +2119,7 @@ class _MapScreenState extends State<MapScreen> {
c.publicKeyHex.substring(0, 12), c.publicKeyHex.substring(0, 12),
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 10.5, fontSize: 10.5,
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
), ),
), ),
], ],
@@ -2070,13 +2129,13 @@ class _MapScreenState extends State<MapScreen> {
Icon( Icon(
Icons.chevron_right, Icons.chevron_right,
size: 18, size: 18,
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
) )
else else
Text( Text(
context.l10n.map_noGps.toUpperCase(), context.l10n.map_noGps.toUpperCase(),
style: MeshTheme.accentLabel( style: MeshTheme.accentLabel(
color: MapPalette.textMuted, color: _overlayMutedTextColor,
fontSize: 8.5, fontSize: 8.5,
), ),
), ),
@@ -2136,14 +2195,14 @@ class _MapScreenState extends State<MapScreen> {
width: 230, width: 230,
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: MapPalette.panelDark, color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md), borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border), border: Border.all(color: _overlayBorderColor),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 10, blurRadius: 10,
offset: Offset(0, 4), offset: const Offset(0, 4),
), ),
], ],
), ),
@@ -2160,7 +2219,7 @@ class _MapScreenState extends State<MapScreen> {
), ),
_statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline), _statRow(context.l10n.map_hidden, hiddenCount, MapPalette.offline),
_statRow(context.l10n.map_markers, pinCount, MapPalette.shared), _statRow(context.l10n.map_markers, pinCount, MapPalette.shared),
const Divider(height: 16, color: MapPalette.border), Divider(height: 16, color: _overlayBorderColor),
_buildLegendItem( _buildLegendItem(
Icons.person, Icons.person,
context.l10n.map_chat, context.l10n.map_chat,
@@ -2190,7 +2249,7 @@ class _MapScreenState extends State<MapScreen> {
_buildLegendItem( _buildLegendItem(
Icons.not_listed_location, Icons.not_listed_location,
context.l10n.map_guessedLocation, context.l10n.map_guessedLocation,
MapPalette.textMuted, _overlayMutedTextColor,
), ),
], ],
), ),
@@ -2211,7 +2270,10 @@ class _MapScreenState extends State<MapScreen> {
Expanded( Expanded(
child: Text( child: Text(
label, label,
style: TextStyle(fontSize: 12.5, color: MapPalette.textSecondary), style: TextStyle(
fontSize: 12.5,
color: _overlaySecondaryTextColor,
),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
@@ -2220,7 +2282,7 @@ class _MapScreenState extends State<MapScreen> {
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
), ),
], ],
@@ -2253,8 +2315,8 @@ class _MapScreenState extends State<MapScreen> {
child: MeshCard( child: MeshCard(
margin: EdgeInsets.zero, margin: EdgeInsets.zero,
padding: const EdgeInsets.fromLTRB(14, 12, 8, 12), padding: const EdgeInsets.fromLTRB(14, 12, 8, 12),
color: MapPalette.panelDark, color: _overlayPanelColor,
borderColor: MapPalette.border, borderColor: _overlayBorderColor,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -2277,10 +2339,10 @@ class _MapScreenState extends State<MapScreen> {
Flexible( Flexible(
child: Text( child: Text(
contact.name, contact.name,
style: const TextStyle( style: TextStyle(
fontSize: 15, fontSize: 15,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -2308,9 +2370,9 @@ class _MapScreenState extends State<MapScreen> {
Flexible( Flexible(
child: Text( child: Text(
contact.typeLabel(context.l10n), contact.typeLabel(context.l10n),
style: const TextStyle( style: TextStyle(
fontSize: 11.5, fontSize: 11.5,
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
@@ -2322,13 +2384,13 @@ class _MapScreenState extends State<MapScreen> {
), ),
if (pos != null) if (pos != null)
IconButton( IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.center_focus_strong, size: 20), icon: const Icon(Icons.center_focus_strong, size: 20),
tooltip: context.l10n.map_centerOnNode, tooltip: context.l10n.map_centerOnNode,
onPressed: () => _mapController.move(pos, max(_zoom, 15)), onPressed: () => _mapController.move(pos, max(_zoom, 15)),
), ),
IconButton( IconButton(
color: MapPalette.textPrimary, color: _overlayPrimaryTextColor,
icon: const Icon(Icons.close, size: 20), icon: const Icon(Icons.close, size: 20),
onPressed: _clearSelection, onPressed: _clearSelection,
), ),
@@ -2345,7 +2407,10 @@ class _MapScreenState extends State<MapScreen> {
), ),
_miniMeta( _miniMeta(
context.l10n.map_path, context.l10n.map_path,
contact.pathLabel(context.l10n), contact.pathLabel(
context.l10n,
pathHashByteWidth: connector.pathHashByteWidth,
),
), ),
_miniMeta('ID', contact.publicKeyHex.substring(0, 12)), _miniMeta('ID', contact.publicKeyHex.substring(0, 12)),
if (pos != null) if (pos != null)
@@ -2389,14 +2454,17 @@ class _MapScreenState extends State<MapScreen> {
Text( Text(
label.toUpperCase(), label.toUpperCase(),
style: MeshTheme.accentLabel( style: MeshTheme.accentLabel(
color: MapPalette.textMuted, color: _overlayMutedTextColor,
fontSize: 8, fontSize: 8,
), ),
), ),
const SizedBox(height: 1), const SizedBox(height: 1),
Text( Text(
value, value,
style: MeshTheme.mono(fontSize: 11.5, color: MapPalette.textPrimary), style: MeshTheme.mono(
fontSize: 11.5,
color: _overlayPrimaryTextColor,
),
), ),
], ],
); );
@@ -2746,7 +2814,10 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
_buildInfoRow( _buildInfoRow(
context.l10n.map_path, context.l10n.map_path,
contact.pathLabel(context.l10n), contact.pathLabel(
context.l10n,
pathHashByteWidth: connector.pathHashByteWidth,
),
), ),
if (contact.hasLocation) if (contact.hasLocation)
_buildInfoRow( _buildInfoRow(
@@ -3492,10 +3563,23 @@ class _MapScreenState extends State<MapScreen> {
} }
void _addToPath(BuildContext context, Contact contact, {LatLng? position}) { void _addToPath(BuildContext context, Contact contact, {LatLng? position}) {
final connector = context.read<MeshCoreConnector>();
final hopWidth = min(
connector.pathHashByteWidth.clamp(1, pubKeySize),
contact.publicKey.length,
).toInt();
final hopPrefix = contact.publicKey.sublist(0, hopWidth);
for (final existingHop in PathHelper.splitPathBytes(
_pathTrace,
connector.pathHashByteWidth,
)) {
if (listEquals(existingHop, hopPrefix)) {
return;
}
}
setState(() { setState(() {
_pathTrace.add( _pathTrace.addAll(hopPrefix); // Add the hop-width pubkey prefix.
contact.publicKey[0], _pathTraceHopWidths.add(hopWidth);
); // Add first 16 bytes of public key to path trace
_pathTraceContacts.add( _pathTraceContacts.add(
contact.copyWith( contact.copyWith(
latitude: position?.latitude ?? contact.latitude, latitude: position?.latitude ?? contact.latitude,
@@ -3510,6 +3594,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() { setState(() {
_isBuildingPathTrace = true; _isBuildingPathTrace = true;
_pathTrace.clear(); _pathTrace.clear();
_pathTraceHopWidths.clear();
_pathTraceContacts.clear(); _pathTraceContacts.clear();
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
@@ -3519,8 +3604,19 @@ class _MapScreenState extends State<MapScreen> {
void _removePath() { void _removePath() {
setState(() { setState(() {
final recordedHopWidth = _pathTraceHopWidths.isNotEmpty
? _pathTraceHopWidths.removeLast()
: context.read<MeshCoreConnector>().pathHashByteWidth.clamp(
1,
pubKeySize,
);
final hopByteCount = min(recordedHopWidth, _pathTrace.length).toInt();
_pathTraceContacts.removeLast(); _pathTraceContacts.removeLast();
_pathTrace.removeLast(); // Remove last node from path trace // A path trace hop can be wider than one byte; remove the full hash prefix.
_pathTrace.removeRange(
_pathTrace.length - hopByteCount,
_pathTrace.length,
);
_points.removeLast(); // Remove last point from points list _points.removeLast(); // Remove last point from points list
_polylines.clear(); // Clear polylines _polylines.clear(); // Clear polylines
}); });
@@ -3537,14 +3633,14 @@ class _MapScreenState extends State<MapScreen> {
right: 16, right: 16,
child: DecoratedBox( child: DecoratedBox(
decoration: BoxDecoration( decoration: BoxDecoration(
color: MapPalette.panelDark, color: _overlayPanelColor,
borderRadius: BorderRadius.circular(MeshRadii.md), borderRadius: BorderRadius.circular(MeshRadii.md),
border: Border.all(color: MapPalette.border), border: Border.all(color: _overlayBorderColor),
boxShadow: const [ boxShadow: [
BoxShadow( BoxShadow(
color: MapPalette.markerShadow, color: _overlayShadowColor,
blurRadius: 10, blurRadius: 10,
offset: Offset(0, 4), offset: const Offset(0, 4),
), ),
], ],
), ),
@@ -3557,24 +3653,34 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
Text( Text(
l10n.contacts_pathTrace, l10n.contacts_pathTrace,
style: TextStyle(fontWeight: FontWeight.bold), style: TextStyle(
fontWeight: FontWeight.bold,
color: _overlayPrimaryTextColor,
),
), ),
if (_pathTrace.isEmpty) const SizedBox(height: 8), if (_pathTrace.isEmpty) const SizedBox(height: 8),
if (_pathTrace.isEmpty) if (_pathTrace.isEmpty)
Text(l10n.map_tapToAdd, style: TextStyle(fontSize: 12)), Text(
l10n.map_tapToAdd,
style: TextStyle(
fontSize: 12,
color: _overlaySecondaryTextColor,
),
),
const SizedBox(height: 6), const SizedBox(height: 6),
if (_pathTrace.isNotEmpty) if (_pathTrace.isNotEmpty)
Text( Text(
"${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}", "${l10n.path_currentPathLabel} ${formatDistance(getPathDistanceMeters(_points), isImperial: isImperial)}",
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 12, fontSize: 12,
color: MapPalette.textSecondary, color: _overlaySecondaryTextColor,
), ),
), ),
SelectableText( SelectableText(
_pathTrace PathHelper.splitPathBytes(
.map((b) => b.toRadixString(16).padLeft(2, '0')) _pathTrace,
.join(','), context.read<MeshCoreConnector>().pathHashByteWidth,
).map(PathHelper.formatHopHex).join(','),
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
@@ -3599,6 +3705,7 @@ class _MapScreenState extends State<MapScreen> {
builder: (context) => PathTraceMapScreen( builder: (context) => PathTraceMapScreen(
title: l10n.contacts_pathTrace, title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace), path: Uint8List.fromList(_pathTrace),
flipPathAround: false,
pathHashByteWidth: hashW, pathHashByteWidth: hashW,
pathContacts: _pathTraceContacts, pathContacts: _pathTraceContacts,
), ),
@@ -3621,6 +3728,10 @@ class _MapScreenState extends State<MapScreen> {
title: l10n.contacts_pathTrace, title: l10n.contacts_pathTrace,
path: Uint8List.fromList(_pathTrace), path: Uint8List.fromList(_pathTrace),
flipPathAround: true, flipPathAround: true,
pathHashByteWidth: context
.read<MeshCoreConnector>()
.pathHashByteWidth,
pathContacts: _pathTraceContacts,
), ),
), ),
); );
@@ -3643,6 +3754,7 @@ class _MapScreenState extends State<MapScreen> {
setState(() { setState(() {
_isBuildingPathTrace = false; _isBuildingPathTrace = false;
_pathTrace.clear(); _pathTrace.clear();
_pathTraceHopWidths.clear();
_points.clear(); _points.clear();
_polylines.clear(); _polylines.clear();
}); });
+2 -2
View File
@@ -276,7 +276,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: Column( title: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
@@ -296,7 +296,7 @@ class _NeighborsScreenState extends State<NeighborsScreen> {
), ),
], ],
), ),
centerTitle: false, centerTitle: true,
actions: [ actions: [
IconButton( IconButton(
icon: Icon(isFloodMode ? Icons.waves : Icons.route), icon: Icon(isFloodMode ? Icons.waves : Icons.route),
+340 -164
View File
@@ -7,6 +7,7 @@ import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart'; import 'package:latlong2/latlong.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart'; import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/helpers/path_helper.dart';
import 'package:meshcore_open/l10n/l10n.dart'; import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/models/app_settings.dart'; import 'package:meshcore_open/models/app_settings.dart';
import 'package:meshcore_open/models/contact.dart'; import 'package:meshcore_open/models/contact.dart';
@@ -19,7 +20,6 @@ import 'package:meshcore_open/services/path_history_service.dart';
import 'package:meshcore_open/utils/app_logger.dart'; import 'package:meshcore_open/utils/app_logger.dart';
import 'package:meshcore_open/widgets/path_map_ui.dart'; import 'package:meshcore_open/widgets/path_map_ui.dart';
import 'package:meshcore_open/widgets/snr_indicator.dart'; import 'package:meshcore_open/widgets/snr_indicator.dart';
import 'package:meshcore_open/widgets/themed_map_tile_layer.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
@@ -27,9 +27,9 @@ export 'package:meshcore_open/widgets/path_map_ui.dart'
show formatDistance, getPathDistanceMeters; show formatDistance, getPathDistanceMeters;
class PathTraceData { class PathTraceData {
final Uint8List pathData; final List<Uint8List> pathData;
final List<double> snrData; final List<double> snrData;
final Map<int, Contact> pathContacts; final Map<String, Contact> pathContacts;
PathTraceData({ PathTraceData({
required this.pathData, required this.pathData,
@@ -38,6 +38,20 @@ class PathTraceData {
}); });
} }
String _hopKey(Uint8List hopBytes) => PathHelper.formatHopHex(hopBytes);
Uint8List _lastHopChunk(Uint8List path, int hopWidth) {
if (path.isEmpty) return Uint8List(0);
final width = hopWidth.clamp(1, path.length).toInt();
return Uint8List.fromList(path.sublist(path.length - width));
}
bool _matchesHopPrefix(Uint8List a, Uint8List b) {
if (a.isEmpty || b.isEmpty) return false;
final width = min(a.length, b.length);
return listEquals(a.sublist(0, width), b.sublist(0, width));
}
class PathTraceMapScreen extends StatefulWidget { class PathTraceMapScreen extends StatefulWidget {
final String title; final String title;
final Uint8List path; final Uint8List path;
@@ -80,8 +94,8 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
bool _failed2Loaded = false; bool _failed2Loaded = false;
bool _hasData = false; bool _hasData = false;
PathTraceData? _traceData; PathTraceData? _traceData;
// Inferred positions for hops that have no GPS location, keyed by hop byte. // Inferred positions for hops that have no GPS location, keyed by hop prefix.
Map<int, LatLng> _inferredHopPositions = {}; Map<String, LatLng> _inferredHopPositions = {};
// Endpoint position for the target contact (GPS or guessed). // Endpoint position for the target contact (GPS or guessed).
LatLng? _targetContactPosition; LatLng? _targetContactPosition;
bool _targetContactIsGuessed = false; bool _targetContactIsGuessed = false;
@@ -94,6 +108,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
double _pathDistanceMeters = 0.0; double _pathDistanceMeters = 0.0;
bool _showNodeLabels = true; bool _showNodeLabels = true;
Contact? _targetContact; Contact? _targetContact;
Uint8List? _sentTagBytes;
// Live path resolved at trace time; used by the response handler for // Live path resolved at trace time; used by the response handler for
// endpoint inference so it matches the path that was actually traced. // endpoint inference so it matches the path that was actually traced.
Uint8List _tracedPath = Uint8List(0); Uint8List _tracedPath = Uint8List(0);
@@ -103,17 +118,82 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
PathHistoryService? _pathHistory; PathHistoryService? _pathHistory;
PathViewMode _viewMode = PathViewMode.single; PathViewMode _viewMode = PathViewMode.single;
List<DisplayPath> _displayPaths = []; List<DisplayPath> _displayPaths = [];
List<int> _primaryOutboundHops = []; List<Uint8List> _primaryOutboundHops = [];
String _selectedPathId = 'primary'; String _selectedPathId = 'primary';
final Set<String> _hiddenPathIds = {}; final Set<String> _hiddenPathIds = {};
bool _panelCollapsed = false; bool _panelCollapsed = false;
bool _animationEnabled = true; bool _animationEnabled = true;
bool _followPacket = false; bool _followPacket = false;
String _formatPathPrefixes(Uint8List pathBytes) { String _formatPathPrefixes(Uint8List pathBytes, [int? hashByteWidth]) {
return pathBytes return PathHelper.splitPathBytes(
.map((b) => b.toRadixString(16).padLeft(2, '0').toUpperCase()) pathBytes,
.join(','); hashByteWidth ?? widget.pathHashByteWidth,
).map(PathHelper.formatHopHex).join(',');
}
int _traceHashByteWidth(int pathHashByteWidth) {
final width = pathHashByteWidth.clamp(1, pubKeySize).toInt();
if (width <= 1) return 1;
if (width == 2) return 2;
// Trace packets encode hash width as 1 << flags, so 3-byte path hashes
// must be traced with a 4-byte public-key prefix.
return 4;
}
int _traceFlagsForHashWidth(int traceHashByteWidth) {
if (traceHashByteWidth <= 1) return 0;
if (traceHashByteWidth == 2) return 1;
return 2;
}
Uint8List _reversePathByHop(Uint8List pathBytes, int hopWidth) {
final reversedHops = PathHelper.splitPathBytes(
pathBytes,
hopWidth,
).reversed;
final bytes = <int>[];
for (final hop in reversedHops) {
bytes.addAll(hop);
}
return Uint8List.fromList(bytes);
}
Uint8List? _expandHopForTrace(
Uint8List hop,
int traceHashByteWidth,
MeshCoreConnector connector,
) {
if (hop.length == traceHashByteWidth) return hop;
final candidates = <Contact>[
...?widget.pathContacts,
if (widget.targetContact != null) widget.targetContact!,
...connector.allContactsUnfiltered,
];
for (final contact in candidates) {
if (contact.publicKey.length < traceHashByteWidth) continue;
if (!listEquals(contact.publicKey.sublist(0, hop.length), hop)) continue;
return Uint8List.fromList(
contact.publicKey.sublist(0, traceHashByteWidth),
);
}
return null;
}
Uint8List? _tracePathFromBytes(
Uint8List pathBytes,
int traceHashByteWidth,
MeshCoreConnector connector,
) {
final hops = PathHelper.splitPathBytes(pathBytes, widget.pathHashByteWidth);
final traceBytes = <int>[];
for (final hop in hops) {
final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
if (traceHop == null) return null;
traceBytes.addAll(traceHop);
}
return Uint8List.fromList(traceBytes);
} }
@override @override
@@ -233,45 +313,62 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
); );
} }
Uint8List buildPath(Uint8List pathBytes) { Uint8List? buildPath(
Uint8List traceBytes; Uint8List pathBytes,
int traceHashByteWidth,
MeshCoreConnector connector,
) {
final pathHops = PathHelper.splitPathBytes(
pathBytes,
widget.pathHashByteWidth,
);
final hopWidth = traceHashByteWidth.clamp(1, pubKeySize).toInt();
if (pathBytes.isEmpty) { // Compute targetPrefix if targetContact is provided
final pk = widget.targetContact?.publicKey; Uint8List? targetPrefix;
final n = widget.pathHashByteWidth.clamp(1, pubKeySize); if (widget.targetContact != null) {
if (pk != null && pk.length >= n) { final pk = widget.targetContact!.publicKey;
return Uint8List.fromList(pk.sublist(0, n)); if (pk.isNotEmpty) {
final len = pk.length >= hopWidth ? hopWidth : pk.length;
targetPrefix = Uint8List.fromList(pk.sublist(0, len));
} }
traceBytes = Uint8List(1);
traceBytes[0] = pk?[0] ?? 0;
return traceBytes;
} }
if (widget.targetContact?.type == advTypeRepeater || final outboundHops = <Uint8List>[];
widget.targetContact?.type == advTypeRoom) { for (final hop in pathHops) {
final len = (pathBytes.length + pathBytes.length + 1); final traceHop = _expandHopForTrace(hop, traceHashByteWidth, connector);
traceBytes = Uint8List(len); if (traceHop == null) return null;
traceBytes[pathBytes.length] = widget.targetContact?.publicKey[0] ?? 0; outboundHops.add(traceHop);
for (int i = 0; i < pathBytes.length; i++) { }
traceBytes[i] = pathBytes[i]; if (targetPrefix != null) {
if (i < pathBytes.length) { // Check if targetPrefix is already the last hop in pathHops to avoid duplication
traceBytes[len - 1 - i] = pathBytes[i]; bool alreadyEndedWithTarget = false;
if (outboundHops.isNotEmpty) {
if (listEquals(outboundHops.last, targetPrefix)) {
alreadyEndedWithTarget = true;
} }
} }
} else { if (!alreadyEndedWithTarget) {
if (pathBytes.length < 2) { outboundHops.add(targetPrefix);
return pathBytes[0] == 0 ? Uint8List(0) : pathBytes;
}
final len = (pathBytes.length + pathBytes.length - 1);
traceBytes = Uint8List(len);
for (int i = 0; i < pathBytes.length; i++) {
traceBytes[i] = pathBytes[i];
if (i < pathBytes.length - 1) {
traceBytes[len - 1 - i] = pathBytes[i];
}
} }
} }
return traceBytes;
if (outboundHops.isEmpty) {
return Uint8List(0);
}
final mirroredHops = <Uint8List>[...outboundHops];
if (outboundHops.length > 1) {
mirroredHops.addAll(
outboundHops.sublist(0, outboundHops.length - 1).reversed,
);
}
final traceBytes = <int>[];
for (final hop in mirroredHops) {
traceBytes.addAll(hop);
}
return Uint8List.fromList(traceBytes);
} }
/// Resolves the path bytes to trace. When tracing a specific contact's /// Resolves the path bytes to trace. When tracing a specific contact's
@@ -301,26 +398,49 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
final connector = Provider.of<MeshCoreConnector>(context, listen: false); final connector = Provider.of<MeshCoreConnector>(context, listen: false);
final traceHashByteWidth = _traceHashByteWidth(widget.pathHashByteWidth);
final livePath = _resolveLivePath(connector); final livePath = _resolveLivePath(connector);
_tracedPath = livePath; _tracedPath = livePath;
final pathTmp = widget.reversePathAround final pathTmp = widget.reversePathAround
? Uint8List.fromList(livePath.reversed.toList()) ? _reversePathByHop(livePath, widget.pathHashByteWidth)
: livePath; : livePath;
final path = widget.flipPathAround ? buildPath(pathTmp) : pathTmp; final path = widget.flipPathAround
? buildPath(pathTmp, traceHashByteWidth, connector)
: _tracePathFromBytes(pathTmp, traceHashByteWidth, connector);
if (path == null) {
if (!mounted) return;
setState(() {
_isLoading = false;
_failed2Loaded = true;
});
return;
}
appLogger.info( appLogger.info(
'Initiating path trace with path: ${_formatPathPrefixes(path)}', 'Initiating path trace with path: '
'${_formatPathPrefixes(path, traceHashByteWidth)}',
tag: 'PathTraceMapScreen', tag: 'PathTraceMapScreen',
noNotify: !mounted, noNotify: !mounted,
); );
final sentTag = DateTime.now().millisecondsSinceEpoch ~/ 1000;
_sentTagBytes = Uint8List(4)
..[0] = sentTag & 0xFF
..[1] = (sentTag >> 8) & 0xFF
..[2] = (sentTag >> 16) & 0xFF
..[3] = (sentTag >> 24) & 0xFF;
final flags = _traceFlagsForHashWidth(traceHashByteWidth);
final tracePayload = path.isEmpty ? Uint8List.fromList([0x00]) : path;
final frame = buildTraceReq( final frame = buildTraceReq(
DateTime.now().millisecondsSinceEpoch ~/ 1000, sentTag,
0, //auth 0, // auth
0, //flag flags, // flag
payload: path, payload: tracePayload,
); );
connector.sendFrame(frame); connector.sendFrame(frame);
} }
@@ -364,15 +484,13 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
// Check if it's a binary response // Check if it's a binary response
if (frame.length > 8 && if (frame.length >= 12 &&
code == pushCodeTraceData && code == pushCodeTraceData &&
listEquals(frame.sublist(4, 8), tagData)) { (listEquals(frame.sublist(4, 8), _sentTagBytes) ||
listEquals(frame.sublist(4, 8), tagData))) {
_timeoutTimer?.cancel(); _timeoutTimer?.cancel();
if (!mounted) return; if (!mounted) return;
frameBuffer.skipBytes(3); //reserved + path length + flag _handleTraceResponse(frame);
if (listEquals(frameBuffer.readBytes(4), tagData)) {
_handleTraceResponse(frame);
}
} }
} catch (e) { } catch (e) {
_timeoutTimer?.cancel(); _timeoutTimer?.cancel();
@@ -393,21 +511,30 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final buffer = BufferReader(frame); final buffer = BufferReader(frame);
try { try {
buffer.skipBytes(2); // Skip push code and reserved byte buffer.skipBytes(2); // Skip push code and reserved byte
int pathLength = buffer.readUInt8(); final pathLenByte = buffer.readUInt8();
final int flags = buffer final flags = buffer.readUInt8();
.readUInt8(); // path_sz = flags & 0x03 (path-hash mode, fw v1.11+)
buffer.skipBytes(4); // Skip tag data buffer.skipBytes(4); // Skip tag data
buffer.skipBytes(4); // Skip auth code buffer.skipBytes(4); // Skip auth code
final int pathSz = flags & 0x03; var width = _traceHashByteWidth(1 << (flags & 0x03));
Uint8List pathData = buffer.readBytes(pathLength); var pathLength = pathLenByte == 0xFF ? 0 : pathLenByte;
if (pathLength > buffer.remaining && (pathLenByte & 0xC0) != 0) {
final packedWidth = ((pathLenByte & 0xC0) >> 6) + 1;
final packedLength = (pathLenByte & 0x3F) * packedWidth;
if (packedLength <= buffer.remaining) {
width = packedWidth;
pathLength = packedLength;
}
}
final pathBytes = buffer.readBytes(pathLength);
final pathData = PathHelper.splitPathBytes(pathBytes, width);
// Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node). // Firmware emits (path_len >> path_sz) hop SNRs plus 1 final SNR (to this node).
final int snrCount = (pathLength >> pathSz) + 1; final snrCount = (pathLength ~/ width) + 1;
List<double> snrData = buffer List<double> snrData = buffer
.readBytes(snrCount) .readBytes(snrCount)
.map((snr) => snr.toSigned(8).toDouble() / 4) .map((snr) => snr.toSigned(8).toDouble() / 4)
.toList(); .toList();
Map<int, Contact> pathContacts = {}; Map<String, Contact> pathContacts = {};
Contact lastContact = Contact( Contact lastContact = Contact(
path: Uint8List(0), path: Uint8List(0),
pathLength: 0, pathLength: 0,
@@ -419,7 +546,12 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
lastSeen: DateTime.now(), lastSeen: DateTime.now(),
); );
if (widget.pathContacts != null) { if (widget.pathContacts != null) {
pathContacts = {for (var c in widget.pathContacts!) c.publicKey[0]: c}; final hopWidth = width.clamp(1, pubKeySize).toInt();
pathContacts = {
for (var c in widget.pathContacts!)
if (c.publicKey.length >= hopWidth)
_hopKey(Uint8List.fromList(c.publicKey.sublist(0, hopWidth))): c,
};
} else { } else {
final contacts = connector.allContactsUnfiltered; final contacts = connector.allContactsUnfiltered;
contacts.where((c) => c.type != advTypeChat).forEach((repeater) { contacts.where((c) => c.type != advTypeChat).forEach((repeater) {
@@ -434,12 +566,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
_maxRepeaterMatchDistanceMeters) { _maxRepeaterMatchDistanceMeters) {
return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches return; //skip reapeaters that are far away from the last one with known GPS, to avoid false matches
} }
for (var repeaterData in pathData) { for (final repeaterData in pathData) {
final hopWidth = repeaterData.length;
if (repeater.publicKey.length < hopWidth) continue;
if (listEquals( if (listEquals(
repeater.publicKey.sublist(0, 1), repeater.publicKey.sublist(0, hopWidth),
Uint8List.fromList([repeaterData]), repeaterData,
)) { )) {
pathContacts[repeaterData] = repeater; pathContacts[_hopKey(repeaterData)] = repeater;
lastContact = repeater; lastContact = repeater;
} }
} }
@@ -448,13 +582,20 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
// For hops with no GPS contact, infer position from other contacts // For hops with no GPS contact, infer position from other contacts
// with known GPS that share the same last-hop byte. // with known GPS that share the same last-hop byte.
final Map<int, LatLng> inferredPositions = {}; final Map<String, LatLng> inferredPositions = {};
for (final hop in pathData) { for (final hop in pathData) {
final contact = pathContacts[hop]; final hopKey = _hopKey(hop);
final contact = pathContacts[hopKey];
if (contact != null && contact.hasLocation) continue; if (contact != null && contact.hasLocation) continue;
final peers = connector.contacts final peers = connector.contacts
.where( .where(
(c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop, (c) =>
c.hasLocation &&
c.path.isNotEmpty &&
_matchesHopPrefix(
_lastHopChunk(c.path, widget.pathHashByteWidth),
hop,
),
) )
.toList(); .toList();
if (peers.isNotEmpty) { if (peers.isNotEmpty) {
@@ -464,7 +605,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final lon = final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.map((c) => c.longitude!).reduce((a, b) => a + b) /
peers.length; peers.length;
inferredPositions[hop] = LatLng(lat, lon); inferredPositions[hopKey] = LatLng(lat, lon);
} }
} }
@@ -486,20 +627,31 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final tc = _targetContact!; final tc = _targetContact!;
if (tc.hasLocation) { if (tc.hasLocation) {
targetPos = LatLng(tc.latitude!, tc.longitude!); targetPos = LatLng(tc.latitude!, tc.longitude!);
} else if (_tracedPath.length > 1) { } else if (pathData.length > 1 || _tracedPath.isNotEmpty) {
// Infer from the last hop: average GPS contacts sharing that hop. // Infer from the last hop: average GPS contacts sharing that hop.
// For a round-trip path (flipPathAround/reversePathAround), the target-side hop // For a round-trip path (flipPathAround/reversePathAround), the target-side hop
// sits in the middle of the symmetric sequence; .last is the local side. // sits in the middle of the symmetric sequence; .last is the local side.
final tracedHops = PathHelper.splitPathBytes(
_tracedPath,
widget.pathHashByteWidth,
);
final hopsForEndpoint = tracedHops.isNotEmpty
? tracedHops
: pathData;
final lastHop = widget.reversePathAround final lastHop = widget.reversePathAround
? _tracedPath.first ? hopsForEndpoint.first
: _tracedPath.last; : hopsForEndpoint.last;
final lastHopKey = _hopKey(lastHop);
final peers = connector.allContacts final peers = connector.allContacts
.where( .where(
(c) => (c) =>
c.hasLocation && c.hasLocation &&
c.path.isNotEmpty && c.path.isNotEmpty &&
c.path.last == lastHop, _matchesHopPrefix(
_lastHopChunk(c.path, widget.pathHashByteWidth),
lastHop,
),
) )
.toList(); .toList();
if (peers.isNotEmpty) { if (peers.isNotEmpty) {
@@ -516,9 +668,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
lon + offsetDeg * sin(angle), lon + offsetDeg * sin(angle),
); );
targetGuessed = true; targetGuessed = true;
} else if (inferredPositions.containsKey(lastHop)) { } else if (inferredPositions.containsKey(lastHopKey)) {
final lat = inferredPositions[lastHop]!.latitude; final lat = inferredPositions[lastHopKey]!.latitude;
final lon = inferredPositions[lastHop]!.longitude; final lon = inferredPositions[lastHopKey]!.longitude;
const offsetDeg = 0.003; const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi; final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
targetPos = LatLng( targetPos = LatLng(
@@ -528,7 +680,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
targetGuessed = true; targetGuessed = true;
} else { } else {
// As a last resort, just place it at the same position as the last hop. // As a last resort, just place it at the same position as the last hop.
final contact = pathContacts[lastHop]; final contact = pathContacts[lastHopKey];
if (contact != null && contact.hasLocation) { if (contact != null && contact.hasLocation) {
const offsetDeg = 0.003; const offsetDeg = 0.003;
final angle = (tc.publicKey[1] / 255.0) * 2 * pi; final angle = (tc.publicKey[1] / 255.0) * 2 * pi;
@@ -546,21 +698,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
_points = <LatLng>[]; _points = <LatLng>[];
_points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!)); _points.add(LatLng(connector.selfLatitude!, connector.selfLongitude!));
int hopLast = 0; String hopLast = '';
int hopLastLast = 0; String hopLastLast = '';
for (final hop in _traceData!.pathData) { for (final hop in _traceData!.pathData) {
if (hop == hopLastLast && widget.flipPathAround) { final hopKey = _hopKey(hop);
if (hopKey == hopLastLast && widget.flipPathAround) {
break; //skip duplicate hops in round-trip paths break; //skip duplicate hops in round-trip paths
} }
final contact = _traceData!.pathContacts[hop]; final contact = _traceData!.pathContacts[hopKey];
if (contact != null && contact.hasLocation) { if (contact != null && contact.hasLocation) {
_points.add(LatLng(contact.latitude!, contact.longitude!)); _points.add(LatLng(contact.latitude!, contact.longitude!));
} else { } else {
final inferred = inferredPositions[hop]; final inferred = inferredPositions[hopKey];
if (inferred != null) _points.add(inferred); if (inferred != null) _points.add(inferred);
} }
hopLastLast = hopLast; hopLastLast = hopLast;
hopLast = hop; hopLast = hopKey;
} }
if (targetPos != null) { if (targetPos != null) {
if (_targetContact != null && _targetContact!.type == advTypeChat) { if (_targetContact != null && _targetContact!.type == advTypeChat) {
@@ -583,7 +736,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
_initialZoom = _points.isNotEmpty ? 13.0 : 2.0; _initialZoom = _points.isNotEmpty ? 13.0 : 2.0;
_bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null; _bounds = _points.length > 1 ? LatLngBounds.fromPoints(_points) : null;
_mapKey = ValueKey( _mapKey = ValueKey(
'${context.l10n.pathTrace_you},${_formatPathPrefixes(_traceData!.pathData)}', '${context.l10n.pathTrace_you},${_traceData!.pathData.map(PathHelper.formatHopHex).join(',')}',
); );
_pathDistanceMeters = getPathDistanceMeters(_points); _pathDistanceMeters = getPathDistanceMeters(_points);
_primaryOutboundHops = _outboundHops(pathData); _primaryOutboundHops = _outboundHops(pathData);
@@ -605,37 +758,47 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
/// Outbound hop bytes of the traced path, mirroring the round-trip /// Outbound hop bytes of the traced path, mirroring the round-trip
/// dedup logic used when building [_points]. /// dedup logic used when building [_points].
List<int> _outboundHops(Uint8List pathData) { List<Uint8List> _outboundHops(List<Uint8List> pathData) {
final hops = <int>[]; final hops = <Uint8List>[];
int hopLast = 0; var hopLast = '';
int hopLastLast = 0; var hopLastLast = '';
for (final hop in pathData) { for (final hop in pathData) {
if (hop == hopLastLast && widget.flipPathAround) break; final hopKey = _hopKey(hop);
if (hopKey == hopLastLast && widget.flipPathAround) break;
hops.add(hop); hops.add(hop);
hopLastLast = hopLast; hopLastLast = hopLast;
hopLast = hop; hopLast = hopKey;
} }
return hops; return hops;
} }
Contact? _contactForHop(int hop, MeshCoreConnector connector) { Contact? _contactForHop(Uint8List hop, MeshCoreConnector connector) {
final traced = _traceData?.pathContacts[hop]; final traced = _traceData?.pathContacts[_hopKey(hop)];
if (traced != null) return traced; if (traced != null) return traced;
for (final c in connector.allContactsUnfiltered) { for (final c in connector.allContactsUnfiltered) {
if (c.type != advTypeChat && if (c.type != advTypeChat &&
c.publicKey.isNotEmpty && c.publicKey.length >= hop.length &&
c.publicKey[0] == hop) { listEquals(c.publicKey.sublist(0, hop.length), hop)) {
return c; return c;
} }
} }
return null; return null;
} }
LatLng? _inferredPositionForHop(int hop, MeshCoreConnector connector) { LatLng? _inferredPositionForHop(Uint8List hop, MeshCoreConnector connector) {
final cached = _inferredHopPositions[hop]; final hopKey = _hopKey(hop);
final cached = _inferredHopPositions[hopKey];
if (cached != null) return cached; if (cached != null) return cached;
final peers = connector.contacts final peers = connector.contacts
.where((c) => c.hasLocation && c.path.isNotEmpty && c.path.last == hop) .where(
(c) =>
c.hasLocation &&
c.path.isNotEmpty &&
_matchesHopPrefix(
_lastHopChunk(c.path, widget.pathHashByteWidth),
hop,
),
)
.toList(); .toList();
if (peers.isEmpty) return null; if (peers.isEmpty) return null;
final lat = final lat =
@@ -643,10 +806,14 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final lon = final lon =
peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length; peers.map((c) => c.longitude!).reduce((a, b) => a + b) / peers.length;
final pos = LatLng(lat, lon); final pos = LatLng(lat, lon);
_inferredHopPositions[hop] = pos; _inferredHopPositions[hopKey] = pos;
return pos; return pos;
} }
String _pathKeyForHops(List<Uint8List> hops) {
return hops.map(PathHelper.formatHopHex).join(',');
}
/// Rebuilds the renderable paths: the traced path as primary plus up to /// Rebuilds the renderable paths: the traced path as primary plus up to
/// four distinct alternates from the target contact's path history. /// four distinct alternates from the target contact's path history.
void _rebuildDisplayPaths(MeshCoreConnector connector) { void _rebuildDisplayPaths(MeshCoreConnector connector) {
@@ -664,18 +831,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final target = widget.targetContact; final target = widget.targetContact;
final history = _pathHistory; final history = _pathHistory;
if (target != null && history != null) { if (target != null && history != null) {
final seen = <String>{_primaryOutboundHops.join(',')}; final seen = <String>{_pathKeyForHops(_primaryOutboundHops)};
var altIndex = 0; var altIndex = 0;
for (final record in history.getRecentPaths(target.publicKeyHex)) { for (final record in history.getRecentPaths(target.publicKeyHex)) {
if (record.pathBytes.isEmpty) continue; if (record.pathBytes.isEmpty) continue;
if (!seen.add(record.pathBytes.join(','))) continue; final recordHops = PathHelper.splitPathBytes(
record.pathBytes,
widget.pathHashByteWidth,
);
if (!seen.add(_pathKeyForHops(recordHops))) continue;
if (altIndex >= kAlternatePathColors.length) break; if (altIndex >= kAlternatePathColors.length) break;
final alt = _buildDisplayPath( final alt = _buildDisplayPath(
id: 'alt-${record.pathBytes.join('-')}', id: 'alt-${_pathKeyForHops(recordHops)}',
label: context.l10n.pathMap_alternate(altIndex + 1), label: context.l10n.pathMap_alternate(altIndex + 1),
color: kAlternatePathColors[altIndex], color: kAlternatePathColors[altIndex],
isPrimary: false, isPrimary: false,
hops: record.pathBytes, hops: recordHops,
record: record, record: record,
connector: connector, connector: connector,
); );
@@ -700,7 +871,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
required String label, required String label,
required Color color, required Color color,
required bool isPrimary, required bool isPrimary,
required List<int> hops, required List<Uint8List> hops,
required MeshCoreConnector connector, required MeshCoreConnector connector,
PathRecord? record, PathRecord? record,
}) { }) {
@@ -719,7 +890,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
for (var i = 0; i < hops.length; i++) { for (var i = 0; i < hops.length; i++) {
final hop = hops[i]; final hop = hops[i];
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final hex = PathHelper.formatHopHex(hop);
final contact = _contactForHop(hop, connector); final contact = _contactForHop(hop, connector);
LatLng? pos; LatLng? pos;
var isGps = false; var isGps = false;
@@ -773,7 +944,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
label: label, label: label,
color: color, color: color,
isPrimary: isPrimary, isPrimary: isPrimary,
hopBytes: List<int>.from(hops), hopBytes: List<Uint8List>.from(hops),
points: points, points: points,
pointLabels: labels, pointLabels: labels,
pointConfirmed: confirmed, pointConfirmed: confirmed,
@@ -934,29 +1105,34 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
List<Marker> _buildHopMarkers( List<Marker> _buildHopMarkers(
List<int> pathData, { List<Uint8List> pathData, {
required bool showLabels, required bool showLabels,
required Contact? target, required Contact? target,
}) { }) {
final markers = <Marker>[]; final markers = <Marker>[];
int hopLast = 0; String hopLast = '';
int hopLastLast = 0; String hopLastLast = '';
for (final hop in pathData) { for (final hop in pathData) {
final contact = _traceData!.pathContacts[hop]; final hopKey = _hopKey(hop);
final inferred = _inferredHopPositions[hop]; final contact = _traceData!.pathContacts[hopKey];
final inferred = _inferredHopPositions[hopKey];
final hasGps = contact != null && contact.hasLocation; final hasGps = contact != null && contact.hasLocation;
if (hop == hopLastLast && widget.flipPathAround) { if (hopKey == hopLastLast && widget.flipPathAround) {
continue; //skip duplicate hops in round-trip paths continue; //skip duplicate hops in round-trip paths
} }
if (!hasGps && inferred == null) { if (!hasGps && inferred == null) {
hopLastLast = hopLast; hopLastLast = hopLast;
hopLast = hop; hopLast = hopKey;
continue; //skip hops with no GPS and no inferred position continue; //skip hops with no GPS and no inferred position
} }
final point = hasGps final point = hasGps
? LatLng(contact.latitude!, contact.longitude!) ? LatLng(contact.latitude!, contact.longitude!)
: inferred!; : inferred!;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final label = PathHelper.formatHopHex(hop);
final shortLabel = label.length > 2 ? label.substring(0, 2) : label;
final fullLabel = label.length > 2
? (contact?.name != null ? '$label: ${contact!.name}' : label)
: (contact?.name ?? label);
markers.add( markers.add(
Marker( Marker(
@@ -989,9 +1165,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
), ),
alignment: Alignment.center, alignment: Alignment.center,
child: Text( child: Text(
hasGps ? label : '~$label', shortLabel,
style: MeshTheme.mono( style: MeshTheme.mono(
fontSize: 10, fontSize: 12,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
color: hasGps ? MeshPalette.signal : MeshPalette.warn, color: hasGps ? MeshPalette.signal : MeshPalette.warn,
), ),
@@ -1001,15 +1177,10 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
), ),
); );
if (showLabels) { if (showLabels) {
markers.add( markers.add(_buildNodeLabelMarker(point: point, label: fullLabel));
_buildNodeLabelMarker(
point: point,
label: contact?.name ?? '~$label',
),
);
} }
hopLastLast = hopLast; hopLastLast = hopLast;
hopLast = hop; hopLast = hopKey;
} }
_addEndpointMarkers(markers, showLabels: showLabels, target: target); _addEndpointMarkers(markers, showLabels: showLabels, target: target);
@@ -1137,17 +1308,22 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final markers = <Marker>[]; final markers = <Marker>[];
// Hop byte -> paths that use it, in display order. // Hop prefix -> paths that use it, in display order.
final hopPaths = <int, List<DisplayPath>>{}; final hopPaths = <String, List<DisplayPath>>{};
final hopsByKey = <String, Uint8List>{};
for (final path in _visiblePaths) { for (final path in _visiblePaths) {
for (final hop in path.hopBytes) { for (final hop in path.hopBytes) {
final list = hopPaths.putIfAbsent(hop, () => []); final hopKey = _hopKey(hop);
hopsByKey[hopKey] = hop;
final list = hopPaths.putIfAbsent(hopKey, () => []);
if (!list.contains(path)) list.add(path); if (!list.contains(path)) list.add(path);
} }
} }
for (final entry in hopPaths.entries) { for (final entry in hopPaths.entries) {
final hop = entry.key; final hopKey = entry.key;
final hop = hopsByKey[hopKey];
if (hop == null) continue;
final paths = entry.value; final paths = entry.value;
final contact = _contactForHop(hop, connector); final contact = _contactForHop(hop, connector);
final hasGps = contact != null && contact.hasLocation; final hasGps = contact != null && contact.hasLocation;
@@ -1155,7 +1331,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
? LatLng(contact.latitude!, contact.longitude!) ? LatLng(contact.latitude!, contact.longitude!)
: _inferredPositionForHop(hop, connector); : _inferredPositionForHop(hop, connector);
if (point == null) continue; if (point == null) continue;
final label = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final label = PathHelper.formatHopHex(hop);
final baseColor = hasGps ? MeshPalette.signal : MeshPalette.warn; final baseColor = hasGps ? MeshPalette.signal : MeshPalette.warn;
final shared = paths.length > 1; final shared = paths.length > 1;
@@ -1240,11 +1416,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
void _showSharedNodeSheet( void _showSharedNodeSheet(
int hop, Uint8List hop,
Contact? contact, Contact? contact,
List<DisplayPath> paths, List<DisplayPath> paths,
) { ) {
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final hex = PathHelper.formatHopHex(hop);
showSharedNodeSheet( showSharedNodeSheet(
context, context,
title: title:
@@ -1291,29 +1467,28 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
String formatDirectionText(PathTraceData pathTraceData, int index) { String formatDirectionText(PathTraceData pathTraceData, int index) {
if (index == 0 || index == pathTraceData.snrData.length - 1) { if (pathTraceData.pathData.isEmpty) {
return context.l10n.pathTrace_you;
}
if (index < 0 || index > pathTraceData.pathData.length) {
return context.l10n.pathTrace_you;
}
if (index == 0 || index == pathTraceData.pathData.length) {
if (index == 0) { if (index == 0) {
return context.l10n.pathTrace_you; return context.l10n.pathTrace_you;
} else { } else {
final contactName = pathTraceData final hop = pathTraceData.pathData.last;
.pathContacts[pathTraceData.pathData[pathTraceData.pathData.length - final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
1]] final hex = PathHelper.formatHopHex(hop);
?.name;
final hex = pathTraceData.pathData[pathTraceData.pathData.length - 1]
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null return contactName != null
? "$hex: $contactName" ? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}"; : "$hex: ${context.l10n.channelPath_unknownRepeater}";
} }
} else { } else {
final contactName = final hop = pathTraceData.pathData[index - 1];
pathTraceData.pathContacts[pathTraceData.pathData[index - 1]]?.name; final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
final hex = pathTraceData.pathData[index - 1] final hex = PathHelper.formatHopHex(hop);
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null return contactName != null
? "$hex: $contactName" ? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}"; : "$hex: ${context.l10n.channelPath_unknownRepeater}";
@@ -1321,14 +1496,18 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
} }
String formatDirectionSubText(PathTraceData pathTraceData, int index) { String formatDirectionSubText(PathTraceData pathTraceData, int index) {
if (index == 0 || index == pathTraceData.snrData.length - 1) { if (pathTraceData.pathData.isEmpty) {
return context.l10n.pathTrace_you;
}
if (index < 0 || index > pathTraceData.pathData.length) {
return context.l10n.pathTrace_you;
}
if (index == 0 || index == pathTraceData.pathData.length) {
if (index == 0) { if (index == 0) {
final contactName = final hop = pathTraceData.pathData.first;
pathTraceData.pathContacts[pathTraceData.pathData[0]]?.name; final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
final hex = pathTraceData.pathData[0] final hex = PathHelper.formatHopHex(hop);
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null return contactName != null
? "$hex: $contactName" ? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}"; : "$hex: ${context.l10n.channelPath_unknownRepeater}";
@@ -1336,12 +1515,9 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
return context.l10n.pathTrace_you; return context.l10n.pathTrace_you;
} }
} else { } else {
final contactName = final hop = pathTraceData.pathData[index];
pathTraceData.pathContacts[pathTraceData.pathData[index]]?.name; final contactName = pathTraceData.pathContacts[_hopKey(hop)]?.name;
final hex = pathTraceData.pathData[index] final hex = PathHelper.formatHopHex(hop);
.toRadixString(16)
.padLeft(2, '0')
.toUpperCase();
return contactName != null return contactName != null
? "$hex: $contactName" ? "$hex: $contactName"
: "$hex: ${context.l10n.channelPath_unknownRepeater}"; : "$hex: ${context.l10n.channelPath_unknownRepeater}";
@@ -1399,7 +1575,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
}, },
), ),
children: [ children: [
ThemedMapTileLayer(tileCache: tileCache), tileCache.buildTileLayer(context),
AnimatedBuilder( AnimatedBuilder(
animation: _playback, animation: _playback,
builder: (context, _) { builder: (context, _) {
@@ -1670,11 +1846,11 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
final connector = context.read<MeshCoreConnector>(); final connector = context.read<MeshCoreConnector>();
final l10n = context.l10n; final l10n = context.l10n;
final hopUseCount = <int, int>{}; final hopUseCount = <String, int>{};
if (_viewMode == PathViewMode.combined) { if (_viewMode == PathViewMode.combined) {
for (final p in _visiblePaths) { for (final p in _visiblePaths) {
for (final hop in p.hopBytes.toSet()) { for (final hopKey in p.hopBytes.map(PathHelper.formatHopHex).toSet()) {
hopUseCount.update(hop, (v) => v + 1, ifAbsent: () => 1); hopUseCount.update(hopKey, (v) => v + 1, ifAbsent: () => 1);
} }
} }
} }
@@ -1690,7 +1866,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
Widget? trailing; Widget? trailing;
if (index < path.hopBytes.length) { if (index < path.hopBytes.length) {
final hop = path.hopBytes[index]; final hop = path.hopBytes[index];
final hex = hop.toRadixString(16).padLeft(2, '0').toUpperCase(); final hex = PathHelper.formatHopHex(hop);
final contact = _contactForHop(hop, connector); final contact = _contactForHop(hop, connector);
title = contact != null title = contact != null
? '$hex: ${contact.name}' ? '$hex: ${contact.name}'
@@ -1703,7 +1879,7 @@ class _PathTraceMapScreenState extends State<PathTraceMapScreen>
: inferred : inferred
? l10n.pathTrace_legendInferred ? l10n.pathTrace_legendInferred
: l10n.pathMap_noLocation; : l10n.pathMap_noLocation;
final sharedCount = hopUseCount[hop] ?? 0; final sharedCount = hopUseCount[_hopKey(hop)] ?? 0;
subtitle = sharedCount > 1 subtitle = sharedCount > 1
? '$status · ${l10n.pathMap_sharedNodeCount(sharedCount)}' ? '$status · ${l10n.pathMap_sharedNodeCount(sharedCount)}'
: status; : status;
+527
View File
@@ -0,0 +1,527 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/l10n/l10n.dart';
import 'package:meshcore_open/models/contact.dart';
import 'package:meshcore_open/storage/region_store.dart';
import 'package:meshcore_open/theme/mesh_theme.dart';
import 'package:meshcore_open/widgets/mesh_ui.dart';
import 'package:provider/provider.dart';
Future<void> pushRegionManagementScreen(BuildContext context) {
return Navigator.push(
context,
MaterialPageRoute<void>(
builder: (context) => const RegionManagementScreen(),
),
);
}
class RegionManagementScreen extends StatefulWidget {
const RegionManagementScreen({super.key});
@override
State<RegionManagementScreen> createState() => _RegionManagementScreenState();
}
class _RegionManagementScreenState extends State<RegionManagementScreen> {
static final RegExp _validFetchedRegion = RegExp(r'^[a-z0-9-]{1,30}$');
final RegionStore _regionStore = RegionStore();
List<Region> _regions = [];
bool _isFetchingRegions = false;
@override
void initState() {
super.initState();
final connector = context.read<MeshCoreConnector>();
_regionStore.setPublicKeyHex = connector.selfPublicKeyHex;
_loadRegions();
}
void _loadRegions() {
final regions = _regionStore.loadRegions();
if (mounted) {
setState(() {
_regions = regions;
});
}
}
@override
Widget build(BuildContext context) {
final l10n = context.l10n;
return Scaffold(
appBar: AppBar(
title: Text(l10n.settings_regionManagement_screenTitle),
centerTitle: true,
actions: [
IconButton(
tooltip: l10n.settings_regionAddRegion,
icon: const Icon(Icons.add),
onPressed: () => _showAddRegionDialog(context),
),
IconButton(
tooltip: l10n.settings_regionFetchRegions,
icon: _isFetchingRegions
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Icon(Icons.travel_explore),
onPressed: _isFetchingRegions ? null : _showFetchRegionsDialog,
),
],
),
body: ListView.builder(
padding: const EdgeInsets.only(left: 16, right: 16, top: 8, bottom: 88),
itemCount: _regions.length,
itemBuilder: (context, index) {
final region = _regions[index];
return _buildRegionTile(context, region);
},
),
);
}
void _showAddRegionDialog(BuildContext context) {
final l10n = context.l10n;
final controller = TextEditingController();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text(l10n.settings_regionName),
content: TextField(
controller: controller,
autofocus: true,
textInputAction: TextInputAction.send,
onSubmitted: (_) => _handleAddRegion(controller.text, context),
decoration: InputDecoration(
hintText: l10n.settings_regionNameHint,
border: const OutlineInputBorder(),
),
inputFormatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp("[a-z0-9-]")),
],
maxLength: 30,
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text(l10n.common_cancel),
),
TextButton(
onPressed: () => _handleAddRegion(controller.text, context),
child: Text(l10n.common_add),
),
],
),
);
}
Future<void> _showFetchRegionsDialog() async {
if (_isFetchingRegions) return;
setState(() {
_isFetchingRegions = true;
});
Set<Region> fetchedRegions = {};
try {
fetchedRegions = await _fetchRegionsFromRepeaters();
} finally {
if (mounted) {
setState(() {
_isFetchingRegions = false;
});
}
}
if (!mounted) return;
final l10n = context.l10n;
final sortedRegions = fetchedRegions.toList()..sort();
await showDialog<void>(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(l10n.settings_regionFetchRegions),
content: sortedRegions.isEmpty
? Text(l10n.settings_regionFetchRegionsFail)
: StatefulBuilder(
builder: (context, setDialogState) {
return SizedBox(
width: double.maxFinite,
child: ListView.builder(
shrinkWrap: true,
itemCount: sortedRegions.length,
itemBuilder: (context, index) {
final fetchedRegion = sortedRegions[index];
final alreadyExists = _regions.contains(fetchedRegion);
return MeshCard(
margin: const EdgeInsets.symmetric(vertical: 4),
padding: const EdgeInsets.only(left: 14, right: 4),
child: Row(
children: [
const Icon(
Icons.landscape,
color: MeshPalette.blue,
),
const SizedBox(width: 12),
Expanded(
child: Text(
fetchedRegion,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
TextButton(
style: alreadyExists
? TextButton.styleFrom(
foregroundColor: Theme.of(
context,
).disabledColor,
)
: null,
onPressed: () {
if (alreadyExists) {
_showDialogSnackBar(
context,
l10n.settings_regionFetchRegionsAlreadyExists,
);
return;
}
_regionStore.addRegion(fetchedRegion);
_loadRegions();
setDialogState(() {});
},
child: Text(l10n.common_add),
),
],
),
);
},
),
);
},
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.common_close),
),
],
),
);
}
void _showDialogSnackBar(BuildContext context, String message) {
final overlay = Overlay.maybeOf(context);
if (overlay == null) return;
final theme = Theme.of(context);
final entry = OverlayEntry(
builder: (context) => Positioned(
left: 16,
right: 16,
bottom: 32,
child: SafeArea(
child: Material(
color: theme.colorScheme.inverseSurface,
elevation: 6,
borderRadius: BorderRadius.circular(4),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
child: Text(
message,
style: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.onInverseSurface,
),
),
),
),
),
),
);
overlay.insert(entry);
Timer(const Duration(seconds: 3), entry.remove);
}
Future<Set<Region>> _fetchRegionsFromRepeaters() async {
final connector = context.read<MeshCoreConnector>();
final repeaters = await _discoverNearbyRepeaters(connector);
final regions = <Region>{};
for (final repeater in repeaters) {
if (!mounted || !connector.isConnected) break;
regions.addAll(await _requestRegionsFromRepeater(connector, repeater));
}
return regions;
}
Future<List<Contact>> _discoverNearbyRepeaters(
MeshCoreConnector connector,
) async {
final repeaters = connector.contacts
.where((contact) => contact.type == advTypeRepeater)
.toList();
if (repeaters.isEmpty || !connector.isConnected) return <Contact>[];
StreamSubscription<Uint8List>? subscription;
Timer? timeout;
final completer = Completer<Set<String>>();
final respondingPrefixes = <String>{};
final tag = DateTime.now().microsecondsSinceEpoch & 0xFFFFFFFF;
void complete() {
if (completer.isCompleted) return;
timeout?.cancel();
subscription?.cancel();
completer.complete(respondingPrefixes);
}
subscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty || completer.isCompleted) return;
final reader = BufferReader(frame);
try {
if (reader.readByte() != pushCodeControlData) return;
if (reader.remaining < 9) return;
reader.skipBytes(3); // SNR, RSSI, path_len from companion firmware.
final payloadType = reader.readByte();
if (((payloadType >> 4) & 0x0F) != controlSubtypeDiscoverResp ||
(payloadType & 0x0F) != advTypeRepeater) {
return;
}
reader.skipBytes(1); // Inbound SNR reported by the responding repeater.
if (reader.readUInt32LE() != tag) return;
final publicKeyPrefix = reader.readRemainingBytes();
if (publicKeyPrefix.isEmpty) return;
respondingPrefixes.add(pubKeyToHex(publicKeyPrefix));
} catch (_) {
// Ignore malformed discovery frames; another response may still arrive.
}
});
try {
final payload = buildDiscoveryRequestPayload(tag, prefixOnly: true);
await connector.sendFrame(buildSendControlDataFrame(payload));
timeout = Timer(const Duration(seconds: 10), complete);
final prefixes = await completer.future;
return repeaters.where((contact) {
final contactKey = contact.publicKeyHex.toLowerCase();
return prefixes.any((prefix) => contactKey.startsWith(prefix));
}).toList();
} catch (_) {
timeout?.cancel();
await subscription.cancel();
return <Contact>[];
}
}
Future<Set<Region>> _requestRegionsFromRepeater(
MeshCoreConnector connector,
Contact repeater,
) async {
StreamSubscription<Uint8List>? subscription;
Timer? timeout;
final completer = Completer<Set<Region>>();
int? expectedTag;
final originalPath = Uint8List.fromList(repeater.path);
final originalPathLength = repeater.pathLength;
var pathChangedForRequest = false;
void complete(Set<Region> regions) {
if (completer.isCompleted) return;
timeout?.cancel();
subscription?.cancel();
completer.complete(regions);
}
void restartTimeout(Duration duration) {
timeout?.cancel();
timeout = Timer(duration, () => complete(<Region>{}));
}
try {
final replyPath = Uint8List(0);
const replyHopCount = 0;
await connector.setContactPath(repeater, replyPath, replyHopCount);
pathChangedForRequest = true;
subscription = connector.receivedFrames.listen((frame) {
if (frame.isEmpty || completer.isCompleted) return;
final reader = BufferReader(frame);
try {
final cmd = reader.readByte();
if (cmd == respCodeSent) {
reader.skipBytes(1);
expectedTag = reader.readUInt32LE();
final estimatedTimeoutMs = reader.readUInt32LE();
restartTimeout(
Duration(
milliseconds: estimatedTimeoutMs > 0
? estimatedTimeoutMs + 2000
: 10000,
),
);
return;
}
if (cmd == respCodeErr) {
complete(<Region>{});
return;
}
if (cmd != pushCodeBinaryResponse || expectedTag == null) return;
reader.skipBytes(1);
final tag = reader.readUInt32LE();
if (tag != expectedTag) return;
complete(_parseRegionsResponse(reader.readRemainingBytes()));
} catch (_) {
complete(<Region>{});
}
});
restartTimeout(const Duration(seconds: 10));
final frame = buildSendAnonReqFrame(
repeater.publicKey,
requestType: anonReqTypeRegions,
replyPath: replyPath,
replyHopCount: replyHopCount,
pathHashWidth: connector.pathHashByteWidth,
);
await connector.sendFrame(frame);
final regions = await completer.future;
if (pathChangedForRequest && connector.isConnected) {
await _restoreRepeaterPath(
connector,
repeater,
originalPathLength,
originalPath,
);
}
return regions;
} catch (_) {
timeout?.cancel();
subscription?.cancel();
if (pathChangedForRequest && connector.isConnected) {
await _restoreRepeaterPath(
connector,
repeater,
originalPathLength,
originalPath,
);
}
return <Region>{};
}
}
Future<void> _restoreRepeaterPath(
MeshCoreConnector connector,
Contact repeater,
int originalPathLength,
Uint8List originalPath,
) async {
if (originalPathLength < 0) {
await connector.clearContactPath(repeater);
return;
}
await connector.setContactPath(repeater, originalPath, originalPathLength);
}
Set<Region> _parseRegionsResponse(Uint8List frame) {
if (frame.length <= 4) return <Region>{};
final names = utf8
.decode(frame.sublist(4), allowMalformed: true)
.replaceAll('\x00', '')
.split(',');
return names
.map((name) => name.trim())
.where((name) => _validFetchedRegion.hasMatch(name))
.toSet();
}
void _handleAddRegion(Region region, BuildContext context) {
Navigator.pop(context);
_regionStore.addRegion(region);
_loadRegions();
}
Widget _buildRegionTile(BuildContext context, Region region) {
final scheme = Theme.of(context).colorScheme;
return MeshCard(
key: ValueKey(region),
padding: const EdgeInsets.only(left: 14, right: 4),
child: Row(
children: [
const Icon(Icons.landscape, color: MeshPalette.blue),
const SizedBox(width: 12),
Expanded(
child: Text(
region,
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
IconButton(
tooltip: context.l10n.settings_deleteRegion,
icon: Icon(Icons.delete_outline, color: scheme.error),
onPressed: () => _confirmDelete(context, region),
),
],
),
);
}
void _confirmDelete(BuildContext context, Region region) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: Text(context.l10n.settings_deleteRegion),
content: Text(context.l10n.settings_deleteRegionConfirm(region)),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(context.l10n.common_cancel),
),
TextButton(
onPressed: () async {
final connector = context.read<MeshCoreConnector>();
Navigator.pop(dialogContext);
await _regionStore.removeRegion(region);
// Deleting a region clears it from any channels that used it;
// refresh the connector's in-memory channel regions to match.
await connector.loadChannelSettings();
_loadRegions();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.settings_regionDeleted)),
);
},
child: Text(
context.l10n.common_delete,
style: TextStyle(
color: Theme.of(dialogContext).colorScheme.error,
),
),
),
],
),
);
}
}
+10 -1
View File
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:meshcore_open/connector/meshcore_protocol.dart'; import 'package:meshcore_open/connector/meshcore_protocol.dart';
import 'package:meshcore_open/connector/meshcore_connector.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
@@ -31,6 +32,7 @@ class RepeaterHubScreen extends StatelessWidget {
final l10n = context.l10n; final l10n = context.l10n;
final scheme = Theme.of(context).colorScheme; final scheme = Theme.of(context).colorScheme;
final settingsService = context.watch<AppSettingsService>(); final settingsService = context.watch<AppSettingsService>();
final connector = context.watch<MeshCoreConnector>();
final chemistry = settingsService.batteryChemistryForRepeater( final chemistry = settingsService.batteryChemistryForRepeater(
repeater.publicKeyHex, repeater.publicKeyHex,
); );
@@ -85,7 +87,10 @@ class RepeaterHubScreen extends StatelessWidget {
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Text( Text(
repeater.pathLabel(l10n), repeater.pathLabel(
l10n,
pathHashByteWidth: connector.pathHashByteWidth,
),
style: Theme.of(context).textTheme.bodySmall style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: scheme.onSurfaceVariant), ?.copyWith(color: scheme.onSurfaceVariant),
), ),
@@ -160,6 +165,10 @@ class RepeaterHubScreen extends StatelessWidget {
value: 'lipo', value: 'lipo',
child: Text(l10n.appSettings_batteryLipo), child: Text(l10n.appSettings_batteryLipo),
), ),
DropdownMenuItem(
value: 'lipo_hv',
child: Text(l10n.appSettings_batteryLipoHv),
),
], ],
), ),
), ),
+173 -1
View File
@@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:typed_data'; import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/contact.dart'; import '../models/contact.dart';
@@ -11,6 +12,7 @@ import '../services/storage_service.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
import '../widgets/mesh_ui.dart'; import '../widgets/mesh_ui.dart';
import '../widgets/routing_sheet.dart'; import '../widgets/routing_sheet.dart';
import '../utils/keys.dart';
import '../helpers/snack_bar_builder.dart'; import '../helpers/snack_bar_builder.dart';
class RepeaterSettingsScreen extends StatefulWidget { class RepeaterSettingsScreen extends StatefulWidget {
@@ -44,6 +46,7 @@ enum _SettingField {
advertInterval, advertInterval,
floodAdvertInterval, floodAdvertInterval,
pathHashMode, pathHashMode,
prvKey,
txDelay, txDelay,
directTxDelay, directTxDelay,
intThresh, intThresh,
@@ -109,6 +112,8 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
bool _refreshingIntThresh = false; bool _refreshingIntThresh = false;
bool _refreshingAgcResetInterval = false; bool _refreshingAgcResetInterval = false;
bool _runningAction = false; bool _runningAction = false;
bool _searchingForKeyPair = false;
bool _stopSearchingForKeyPair = false;
StreamSubscription<Uint8List>? _frameSubscription; StreamSubscription<Uint8List>? _frameSubscription;
RepeaterCommandService? _commandService; RepeaterCommandService? _commandService;
@@ -154,6 +159,11 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
// Advanced // Advanced
int _pathHashMode = 0; // 0-2 int _pathHashMode = 0; // 0-2
final TextEditingController _prvKeyController = TextEditingController();
final TextEditingController _pubKeyController = TextEditingController();
final TextEditingController _prvKeyPrefixController = TextEditingController();
final KeyPairSearcher _keyPairSearcher = KeyPairSearcher();
final int _searchChunkTime = 30; // time in ms to search before yielding
final TextEditingController _txDelayController = TextEditingController(); final TextEditingController _txDelayController = TextEditingController();
final TextEditingController _directTxDelayController = final TextEditingController _directTxDelayController =
TextEditingController(); TextEditingController();
@@ -206,6 +216,10 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_txDelayController.dispose(); _txDelayController.dispose();
_directTxDelayController.dispose(); _directTxDelayController.dispose();
_intThreshController.dispose(); _intThreshController.dispose();
_prvKeyController.dispose();
_pubKeyController.dispose();
_prvKeyPrefixController.dispose();
_stopSearchingForKeyPair = true;
super.dispose(); super.dispose();
} }
@@ -860,6 +874,12 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
command: 'set path.hash.mode $_pathHashMode', command: 'set path.hash.mode $_pathHashMode',
)); ));
} }
if (_dirtyFields.contains(_SettingField.prvKey)) {
final v = _prvKeyController.text.trim();
if (v != "") {
pending.add((field: _SettingField.prvKey, command: 'set prv.key $v'));
}
}
if (_dirtyFields.contains(_SettingField.txDelay) && if (_dirtyFields.contains(_SettingField.txDelay) &&
_txDelayController.text.isNotEmpty) { _txDelayController.text.isNotEmpty) {
final v = double.tryParse(_txDelayController.text.trim()); final v = double.tryParse(_txDelayController.text.trim());
@@ -1005,6 +1025,36 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
} }
} }
void _startSearchingForKeyPair() async {
if (_searchingForKeyPair) return;
setState(() {
_searchingForKeyPair = true;
_stopSearchingForKeyPair = false;
});
_searchForKeyPair();
}
void _searchForKeyPair() async {
if (_stopSearchingForKeyPair) {
if (mounted) setState(() => _searchingForKeyPair = false);
return;
}
final String prefix = _prvKeyPrefixController.text.trim();
MCKeyPair? keys = await _keyPairSearcher.findMatchingKeyPair(
prefix,
_searchChunkTime,
);
if (keys == null) {
// Ran out of time; schedule another attempt.
Timer.run(_searchForKeyPair);
} else {
setState(() => _searchingForKeyPair = false);
_prvKeyController.text = pubKeyToHex(keys.private);
_pubKeyController.text = pubKeyToHex(keys.public);
_markChanged(_SettingField.prvKey);
}
}
Widget _buildInlineRefreshButton({ Widget _buildInlineRefreshButton({
required bool isRefreshing, required bool isRefreshing,
required VoidCallback onRefresh, required VoidCallback onRefresh,
@@ -1069,6 +1119,7 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
_buildOwnerInfoCard(), _buildOwnerInfoCard(),
_buildActionsCard(), _buildActionsCard(),
_buildAdvancedCard(), _buildAdvancedCard(),
_buildKeysCard(),
const SizedBox(height: 16), const SizedBox(height: 16),
_buildDangerZoneCard(), _buildDangerZoneCard(),
], ],
@@ -1984,6 +2035,127 @@ class _RepeaterSettingsScreenState extends State<RepeaterSettingsScreen> {
); );
} }
Widget _buildKeysCard() {
final l10n = context.l10n;
return MeshCard(
child: ExpansionTile(
leading: const Icon(Icons.vpn_key),
title: Text(
l10n.repeater_keySettings,
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w600),
),
subtitle: Text(l10n.repeater_keySettingsSubtitle),
childrenPadding: const EdgeInsets.fromLTRB(0, 8, 0, 4),
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
controller: _prvKeyController,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
],
decoration: InputDecoration(
labelText: l10n.repeater_prvKey,
helperText: l10n.repeater_prvKeyHelper,
helperMaxLines: 3,
border: const OutlineInputBorder(),
),
onChanged: (_) {
_pubKeyController.text = "";
if (_prvKeyController.text.isNotEmpty) {
_markChanged(_SettingField.prvKey);
}
},
),
),
const SizedBox(width: 8),
Padding(
padding: const EdgeInsets.only(top: 8),
child: _searchingForKeyPair
? IconButton(
icon: const Icon(Icons.cancel, size: 24),
onPressed: (() => _stopSearchingForKeyPair = true),
tooltip: l10n.repeater_stopGeneratingPrvKey,
visualDensity: VisualDensity.compact,
)
: IconButton(
icon: const Icon(Icons.casino_rounded, size: 24),
onPressed: () {
_startSearchingForKeyPair();
},
tooltip: l10n.repeater_generatePrvKey,
visualDensity: VisualDensity.compact,
),
),
],
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
readOnly: true,
controller: _pubKeyController,
style: TextStyle(
color: Theme.of(
context,
).textTheme.headlineSmall?.color?.withValues(alpha: 0.6),
),
decoration: InputDecoration(
labelText: l10n.repeater_pubKey,
helperText: l10n.repeater_pubKeyHelper,
helperMaxLines: 3,
border: const OutlineInputBorder(),
),
),
),
const SizedBox(width: 16),
Padding(
padding: const EdgeInsets.only(top: 12),
child: SizedBox(
width: 24,
height: 24,
child: _searchingForKeyPair
? CircularProgressIndicator(strokeWidth: 2)
: null,
),
),
const SizedBox(width: 8),
],
),
const SizedBox(height: 16),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
controller: _prvKeyPrefixController,
maxLength: 4,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp("[0-9a-fA-F]")),
],
onChanged: (_) => setState(() => {}), // updates helper text
decoration: InputDecoration(
labelText: l10n.repeater_pubKeyPrefix,
helperText: l10n.repeater_pubKeyPrefixHelper(
pow(16, _prvKeyPrefixController.text.length).toInt(),
),
helperMaxLines: 3,
border: const OutlineInputBorder(),
),
),
),
const SizedBox(width: 48),
],
),
],
),
);
}
Widget _buildDangerZoneCard() { Widget _buildDangerZoneCard() {
final l10n = context.l10n; final l10n = context.l10n;
return MeshCard( return MeshCard(
+152 -4
View File
@@ -9,6 +9,7 @@ import '../connector/meshcore_connector.dart';
import '../connector/meshcore_protocol.dart'; import '../connector/meshcore_protocol.dart';
import '../l10n/l10n.dart'; import '../l10n/l10n.dart';
import '../models/radio_settings.dart'; import '../models/radio_settings.dart';
import '../services/app_settings_service.dart';
import '../services/app_debug_log_service.dart'; import '../services/app_debug_log_service.dart';
import '../theme/mesh_theme.dart'; import '../theme/mesh_theme.dart';
import '../widgets/app_bar.dart'; import '../widgets/app_bar.dart';
@@ -19,6 +20,7 @@ import 'app_debug_log_screen.dart';
import 'ble_debug_log_screen.dart'; import 'ble_debug_log_screen.dart';
import '../widgets/radio_stats_entry.dart'; import '../widgets/radio_stats_entry.dart';
import '../widgets/sync_progress_overlay.dart'; import '../widgets/sync_progress_overlay.dart';
import 'region_management_screen.dart';
/// Convert device coding-rate value (1-4 on some firmware, 5-8 on others) /// Convert device coding-rate value (1-4 on some firmware, 5-8 on others)
/// to the UI enum range (always 5-8). /// to the UI enum range (always 5-8).
@@ -278,6 +280,18 @@ class _SettingsScreenState extends State<SettingsScreen> {
value: connector.deviceIdLabel, value: connector.deviceIdLabel,
), ),
_buildBatteryInfoRow(context, connector), _buildBatteryInfoRow(context, connector),
if (connector.manufacturerName != null)
_infoRow(
context,
label: l10n.settings_infoHardware,
value: connector.manufacturerName!,
),
if (connector.firmwareVersionString != null)
_infoRow(
context,
label: l10n.settings_infoFirmware,
value: connector.firmwareVersionString!,
),
if (connector.selfName != null) if (connector.selfName != null)
_infoRow( _infoRow(
context, context,
@@ -449,6 +463,14 @@ class _SettingsScreenState extends State<SettingsScreen> {
onTap: () => _showRadioSettings(context, connector), onTap: () => _showRadioSettings(context, connector),
), ),
const Divider(height: 1, indent: 16), const Divider(height: 1, indent: 16),
_tappableTile(
context,
icon: Icons.landscape,
title: l10n.settings_regionSettings,
subtitle: l10n.settings_regionSettingsSubtitle,
onTap: () => pushRegionManagementScreen(context),
),
const Divider(height: 1, indent: 16),
_tappableTile( _tappableTile(
context, context,
icon: Icons.sensors_outlined, icon: Icons.sensors_outlined,
@@ -458,10 +480,30 @@ class _SettingsScreenState extends State<SettingsScreen> {
? () => pushCompanionRadioStatsScreen(context) ? () => pushCompanionRadioStatsScreen(context)
: null, : null,
), ),
const Divider(height: 1, indent: 16),
_tappableTile(
context,
icon: Icons.route_outlined,
title: l10n.repeater_pathHashMode,
subtitle: _pathHashModeSubtitle(context, connector.pathHashByteWidth),
onTap: connector.isConnected
? () => _editPathHashMode(context, connector)
: null,
),
], ],
); );
} }
String _pathHashModeSubtitle(BuildContext context, int pathHashByteWidth) {
final l10n = context.l10n;
return switch (pathHashByteWidth.clamp(1, 4).toInt()) {
1 => l10n.repeater_pathHashModeOption0,
2 => l10n.repeater_pathHashModeOption1,
3 => l10n.repeater_pathHashModeOption2,
_ => l10n.repeater_pathHashModeOption3,
};
}
Widget _buildLocationCardContent( Widget _buildLocationCardContent(
BuildContext context, BuildContext context,
MeshCoreConnector connector, MeshCoreConnector connector,
@@ -750,8 +792,90 @@ class _SettingsScreenState extends State<SettingsScreen> {
); );
} }
void _editPathHashMode(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n;
var selectedMode = (connector.pathHashByteWidth - 1).clamp(0, 3).toInt();
showDialog(
context: context,
builder: (dialogContext) => StatefulBuilder(
builder: (context, setDialogState) => AlertDialog(
title: Text(l10n.repeater_pathHashMode),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DropdownButtonFormField<int>(
initialValue: selectedMode,
decoration: InputDecoration(
labelText: l10n.repeater_pathHashMode,
border: const OutlineInputBorder(),
),
items: [
DropdownMenuItem(
value: 0,
child: Text(l10n.repeater_pathHashModeOption0),
),
DropdownMenuItem(
value: 1,
child: Text(l10n.repeater_pathHashModeOption1),
),
DropdownMenuItem(
value: 2,
child: Text(l10n.repeater_pathHashModeOption2),
),
DropdownMenuItem(
value: 3,
child: Text(l10n.repeater_pathHashModeOption3),
),
],
onChanged: (value) {
if (value == null) return;
setDialogState(() => selectedMode = value);
},
),
const SizedBox(height: 12),
Text(
l10n.repeater_pathHashModeHelper,
style: Theme.of(context).textTheme.bodySmall,
),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(dialogContext),
child: Text(l10n.common_cancel),
),
FilledButton(
onPressed: () async {
Navigator.pop(dialogContext);
try {
await connector.setPathHashMode(selectedMode);
await connector.refreshDeviceInfo();
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.repeater_settingsSaved),
);
} catch (e) {
if (!context.mounted) return;
showDismissibleSnackBar(
context,
content: Text(l10n.settings_error(e.toString())),
);
}
},
child: Text(l10n.common_save),
),
],
),
),
);
}
void _editLocation(BuildContext context, MeshCoreConnector connector) { void _editLocation(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
final settingsService = context.read<AppSettingsService>();
final latController = TextEditingController(); final latController = TextEditingController();
final lonController = TextEditingController(); final lonController = TextEditingController();
final intervalController = TextEditingController(); final intervalController = TextEditingController();
@@ -763,9 +887,9 @@ class _SettingsScreenState extends State<SettingsScreen> {
final bool hasGPS = customVars.containsKey("gps"); final bool hasGPS = customVars.containsKey("gps");
bool isGPSEnabled = customVars["gps"] == "1"; bool isGPSEnabled = customVars["gps"] == "1";
// Read current interval or default to 900 (15 minutes) final currentInterval = settingsService.resolvedGpsIntervalSeconds(
final currentInterval = customVars,
int.tryParse(customVars["gps_interval"] ?? "") ?? 900; );
intervalController.text = currentInterval.toString(); intervalController.text = currentInterval.toString();
String? intervalError; String? intervalError;
@@ -863,7 +987,11 @@ class _SettingsScreenState extends State<SettingsScreen> {
Navigator.pop(context); Navigator.pop(context);
if (interval != null) { if (interval != null) {
await connector.setCustomVar("gps_interval:$interval"); await settingsService.setGpsIntervalSeconds(
interval,
writeToDevice: (value) =>
connector.setCustomVar("gps_interval:$value"),
);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(
@@ -1163,12 +1291,15 @@ class _SettingsScreenState extends State<SettingsScreen> {
void _privacySettings(BuildContext context, MeshCoreConnector connector) { void _privacySettings(BuildContext context, MeshCoreConnector connector) {
final l10n = context.l10n; final l10n = context.l10n;
final settingsService = context.read<AppSettingsService>();
int telemetryMode = connector.telemetryModeBase; int telemetryMode = connector.telemetryModeBase;
int telemetryLocMode = connector.telemetryModeLoc; int telemetryLocMode = connector.telemetryModeLoc;
int telemetryEnvMode = connector.telemetryModeEnv; int telemetryEnvMode = connector.telemetryModeEnv;
bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true; bool advertLocPolicy = connector.advertLocationPolicy == 0 ? false : true;
int multiAcks = connector.multiAcks; int multiAcks = connector.multiAcks;
bool autoZeroHopAdvertOnGpsUpdate =
settingsService.settings.autoSendZeroHopAdvertOnGpsUpdate;
final telemModeBase = [ final telemModeBase = [
DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)), DropdownMenuItem(value: teleModeDeny, child: Text(l10n.settings_denyAll)),
@@ -1203,6 +1334,20 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
}, },
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
FeatureToggleRow(
title: l10n.settings_autoZeroHopAdvertOnGpsUpdate,
subtitle: l10n.settings_autoZeroHopAdvertOnGpsUpdateSubtitle,
value: autoZeroHopAdvertOnGpsUpdate,
enabled: advertLocPolicy,
onChanged: advertLocPolicy
? (value) {
setDialogState(
() => autoZeroHopAdvertOnGpsUpdate = value,
);
}
: null,
),
const SizedBox(height: 8),
SwitchListTile( SwitchListTile(
title: Text(l10n.settings_multiAck), title: Text(l10n.settings_multiAck),
value: multiAcks == 1, value: multiAcks == 1,
@@ -1271,6 +1416,9 @@ void _privacySettings(BuildContext context, MeshCoreConnector connector) {
advertLocPolicy ? 1 : 0, advertLocPolicy ? 1 : 0,
multiAcks, multiAcks,
); );
await settingsService.setAutoSendZeroHopAdvertOnGpsUpdate(
autoZeroHopAdvertOnGpsUpdate,
);
await connector.refreshDeviceInfo(); await connector.refreshDeviceInfo();
if (!context.mounted) return; if (!context.mounted) return;
showDismissibleSnackBar( showDismissibleSnackBar(

Some files were not shown because too many files have changed in this diff Show More