mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Compare commits
153 Commits
v2026.0308
...
v2026.0319
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5cc81bef65 | ||
|
|
6145462555 | ||
|
|
721fb7e7a2 | ||
|
|
5a58e45e13 | ||
|
|
de4258c0a4 | ||
|
|
d8cb35c595 | ||
|
|
8da69705ee | ||
|
|
e474168ba8 | ||
|
|
03dba8b3ce | ||
|
|
bc9e37bd22 | ||
|
|
a32d5d2c85 | ||
|
|
e683ab933f | ||
|
|
f286b0afd1 | ||
|
|
68ec72534d | ||
|
|
6af8b62d9b | ||
|
|
01fbb7de6c | ||
|
|
9303d24b69 | ||
|
|
140088fe2f | ||
|
|
67d8e1c906 | ||
|
|
29b75a3155 | ||
|
|
87cb890877 | ||
|
|
095458ece4 | ||
|
|
5659242672 | ||
|
|
3da4ba8fb2 | ||
|
|
89dc1f2862 | ||
|
|
251932bb47 | ||
|
|
b48139964a | ||
|
|
d29b85feb8 | ||
|
|
c01ef7a059 | ||
|
|
fa2b586ab7 | ||
|
|
bbd5d108cb | ||
|
|
e17f24a6bc | ||
|
|
372224a62c | ||
|
|
e4c8709ee2 | ||
|
|
3c469b62d0 | ||
|
|
1e97ba9831 | ||
|
|
6187067017 | ||
|
|
cd165d2372 | ||
|
|
59f6df4260 | ||
|
|
464fc5e715 | ||
|
|
a76ee4f8f6 | ||
|
|
c884f81ca5 | ||
|
|
993563f059 | ||
|
|
cfd233d38e | ||
|
|
9b14ff4e1a | ||
|
|
84cd8eec00 | ||
|
|
434d3df53e | ||
|
|
1fe260b95e | ||
|
|
d3d53995a6 | ||
|
|
efbddc8831 | ||
|
|
0e3acfa4ca | ||
|
|
450b8382e8 | ||
|
|
420690d683 | ||
|
|
fedd7b9a2b | ||
|
|
c82e0c3452 | ||
|
|
a55cfffa59 | ||
|
|
2bb5b353bd | ||
|
|
991421d097 | ||
|
|
1dcd85f178 | ||
|
|
5d2f61749e | ||
|
|
69377d90a4 | ||
|
|
3e559a4f4e | ||
|
|
a124e3d9f2 | ||
|
|
3bf4971279 | ||
|
|
e355f28ca9 | ||
|
|
7a9be3eb76 | ||
|
|
650f3c66aa | ||
|
|
df993cd176 | ||
|
|
feb3145854 | ||
|
|
da40acf950 | ||
|
|
66eb32bf44 | ||
|
|
c2e0fa952e | ||
|
|
4557fb4b9e | ||
|
|
c05de2cf0a | ||
|
|
c5145c2fca | ||
|
|
e502bc23b7 | ||
|
|
62bf33c9d1 | ||
|
|
7be1460ef6 | ||
|
|
30c273c656 | ||
|
|
5712feba53 | ||
|
|
6de12225fc | ||
|
|
6e9e9e397d | ||
|
|
a3147e9d7c | ||
|
|
5e1912d9f2 | ||
|
|
b42328bd82 | ||
|
|
1b801a10ef | ||
|
|
06632ee32d | ||
|
|
e7a481f97b | ||
|
|
9f31f50e61 | ||
|
|
d4237b7569 | ||
|
|
b35c11c804 | ||
|
|
be2cf53374 | ||
|
|
13a729492f | ||
|
|
9d8594fe05 | ||
|
|
249268dd00 | ||
|
|
63aa9619e6 | ||
|
|
a7ad4bb8ec | ||
|
|
636e114428 | ||
|
|
d5684c51c7 | ||
|
|
ced39b6992 | ||
|
|
083b715039 | ||
|
|
36584be0ab | ||
|
|
dc19296804 | ||
|
|
40072175e3 | ||
|
|
11f3bf9f97 | ||
|
|
24e715d01e | ||
|
|
6e17de3866 | ||
|
|
90877a1891 | ||
|
|
77e43843bf | ||
|
|
1e70f52069 | ||
|
|
5e404944e9 | ||
|
|
53daf2c9a7 | ||
|
|
c36d63a0ad | ||
|
|
69ec2dfdb5 | ||
|
|
d2e6b692f3 | ||
|
|
baa1aa6edd | ||
|
|
38cb3dc031 | ||
|
|
abb73a4c65 | ||
|
|
2d1e6c0ca4 | ||
|
|
b38a8c55d6 | ||
|
|
ca872f2049 | ||
|
|
1b6ff59af3 | ||
|
|
e31bfac2ac | ||
|
|
99bb446ee9 | ||
|
|
088f3d37ef | ||
|
|
7f200e5334 | ||
|
|
ae16417886 | ||
|
|
467371946d | ||
|
|
2d23328ad0 | ||
|
|
0dfb84bf4e | ||
|
|
193a193e8a | ||
|
|
c1d1ec7d0c | ||
|
|
b415b63b4b | ||
|
|
818b9dbfb2 | ||
|
|
8085b89c07 | ||
|
|
86771a3c55 | ||
|
|
4b668ac2e9 | ||
|
|
9deb563c05 | ||
|
|
15ac3e2798 | ||
|
|
97addf7ba6 | ||
|
|
025bd737e7 | ||
|
|
07b675dec6 | ||
|
|
d675ce18dd | ||
|
|
a8e31bb2e8 | ||
|
|
3cd96ba961 | ||
|
|
a98fc6314e | ||
|
|
ae1ea71f58 | ||
|
|
4dff79cd7b | ||
|
|
cbcb329c70 | ||
|
|
7abf96abf2 | ||
|
|
13bf74178c | ||
|
|
66a00c9569 | ||
|
|
b60fa008eb |
23
.github/workflows/build-artifacts.yml
vendored
23
.github/workflows/build-artifacts.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Cache Android NDK
|
||||
uses: actions/cache@v4
|
||||
@@ -95,6 +98,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Linux desktop
|
||||
run: flutter config --enable-linux-desktop
|
||||
@@ -132,6 +138,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable Windows desktop
|
||||
shell: pwsh
|
||||
@@ -174,6 +183,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable macOS desktop
|
||||
run: flutter config --enable-macos-desktop
|
||||
@@ -181,6 +193,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
- name: Install macOS native build tools
|
||||
run: brew install automake libtool
|
||||
|
||||
- name: Build macOS release
|
||||
run: flutter build macos --release
|
||||
|
||||
@@ -216,6 +231,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
@@ -272,6 +290,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: stable
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Enable web
|
||||
run: flutter config --enable-web
|
||||
@@ -280,7 +301,7 @@ jobs:
|
||||
run: flutter pub get
|
||||
|
||||
- name: Build web release
|
||||
run: flutter build web --release --base-href /meshcore_sar_app/
|
||||
run: flutter build web --release --base-href /meshcore-sar/
|
||||
|
||||
- name: Upload pages artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
|
||||
3
.github/workflows/build-multiplatform.yml
vendored
3
.github/workflows/build-multiplatform.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
channel: 'stable'
|
||||
cache: true
|
||||
pub-cache: true
|
||||
cache-key: flutter-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}
|
||||
pub-cache-key: flutter-pub-${{ runner.os }}-stable-${{ env.FLUTTER_VERSION }}-${{ runner.arch }}-${{ hashFiles('pubspec.lock') }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: flutter pub get
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -28,6 +28,7 @@ migrate_working_dir/
|
||||
**/ios/Flutter/.last_build_id
|
||||
.dart_tool/
|
||||
.flutter-plugins-dependencies
|
||||
pubspec_overrides.yaml
|
||||
.pub-cache/
|
||||
.pub/
|
||||
/build/
|
||||
@@ -80,10 +81,11 @@ create_feature_graphic.py
|
||||
# Tool caches and local state
|
||||
.cachebro/
|
||||
.osgrep/
|
||||
third_party/lpcnet_flutter/
|
||||
|
||||
# Claude Code local settings (permissions, personal config)
|
||||
.claude/settings.local.json
|
||||
|
||||
# Dart code coverage
|
||||
coverage/
|
||||
lcov.info
|
||||
lcov.info
|
||||
|
||||
13
CHANGELOG.md
Normal file
13
CHANGELOG.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-03-08 to 2026-03-14
|
||||
|
||||
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.
|
||||
- Messaging and channels: fixed channel sync, deduplication, overwrite and deletion issues, cleared message input after send, and added delayed-message warning tracking.
|
||||
- Voice and transport: improved voice playback quality, improved scan and advert parsing, and kept route path data visible in logs.
|
||||
- Maps and field operations: enabled iOS background GPS updates, added GPX trail import/export, introduced a fullscreen repeater map, and added live mesh traffic views.
|
||||
- Localization and UI polish: added multilingual localization coverage, updated locale strings, fixed onboarding layout issues, and refined channel and contacts presentation.
|
||||
|
||||
## Key PRs
|
||||
|
||||
- [#16](https://github.com/dz0ny/meshcore-sar/pull/16) Merge pull request #16 from `MGJ520/main`
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
|
||||
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
|
||||
`iOS TestFlight:` https://testflight.apple.com/join/HhzerdHp
|
||||
`iOS TestFlight:` https://testflight.apple.com/join/nCZYMPPz
|
||||
|
||||
## Highlights
|
||||
|
||||
|
||||
@@ -84,6 +84,8 @@ PODS:
|
||||
- Flutter
|
||||
- vibration (3.0.0):
|
||||
- Flutter
|
||||
- wakelock_plus (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||
@@ -108,6 +110,7 @@ DEPENDENCIES:
|
||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -161,6 +164,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
vibration:
|
||||
:path: ".symlinks/plugins/vibration/ios"
|
||||
wakelock_plus:
|
||||
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||
@@ -189,6 +194,7 @@ SPEC CHECKSUMS:
|
||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
|
||||
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
||||
|
||||
|
||||
@@ -489,7 +489,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -511,7 +511,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -530,7 +530,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -547,7 +547,7 @@
|
||||
buildSettings = {
|
||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
MARKETING_VERSION = 1.0;
|
||||
@@ -679,7 +679,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
@@ -702,7 +702,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CLANG_ENABLE_MODULES = YES;
|
||||
CURRENT_PROJECT_VERSION = 106;
|
||||
CURRENT_PROJECT_VERSION = 122;
|
||||
DEVELOPMENT_TEAM = JND55328G8;
|
||||
ENABLE_BITCODE = NO;
|
||||
INFOPLIST_FILE = Runner/Info.plist;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import UIKit
|
||||
import Flutter
|
||||
import UserNotifications
|
||||
|
||||
@main
|
||||
@objc class AppDelegate: FlutterAppDelegate {
|
||||
@@ -8,6 +9,7 @@ import Flutter
|
||||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> Bool {
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
UNUserNotificationCenter.current().delegate = self as UNUserNotificationCenterDelegate
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>106</string>
|
||||
<string>122</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
@@ -102,6 +102,7 @@
|
||||
<key>UIBackgroundModes</key>
|
||||
<array>
|
||||
<string>location</string>
|
||||
<string>bluetooth-central</string>
|
||||
<string>bluetooth-peripheral</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
|
||||
@@ -5,24 +5,22 @@
|
||||
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000243">
|
||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000217">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.511584">
|
||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.503497">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="95.467725">
|
||||
<testcase classname="fastlane.lanes" name="2: build_app" time="103.12875">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="240.60994">
|
||||
|
||||
<failure message="/opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/actions/actions_helper.rb:67:in 'Fastlane::Actions.execute_action' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:255:in 'block in Fastlane::Runner#execute_action' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:229:in 'Dir.chdir' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:229:in 'Fastlane::Runner#execute_action' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:157:in 'Fastlane::Runner#trigger_action_by_name' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/fast_file.rb:159:in 'Fastlane::FastFile#method_missing' Fastfile:23:in 'block (2 levels) in Fastlane::FastFile#parsing_binding' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/lane.rb:41:in 'Fastlane::Lane#call' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:49:in 'block in Fastlane::Runner#execute' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:45:in 'Dir.chdir' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/runner.rb:45:in 'Fastlane::Runner#execute' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/lane_manager.rb:46:in 'Fastlane::LaneManager.cruise_lane' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/command_line_handler.rb:34:in 'Fastlane::CommandLineHandler.handle' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:110:in 'block (2 levels) in Fastlane::CommandsGenerator#run' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:187:in 'Commander::Command#call' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/command.rb:157:in 'Commander::Command#run' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/runner.rb:444:in 'Commander::Runner#run_active_command' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane_core/lib/fastlane_core/ui/fastlane_runner.rb:124:in 'Commander::Runner#run!' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/commander-4.6.0/lib/commander/delegates.rb:18:in 'Commander::Delegates#run!' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:363:in 'Fastlane::CommandsGenerator#run' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/commands_generator.rb:43:in 'Fastlane::CommandsGenerator.start' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/fastlane/lib/fastlane/cli_tools_distributor.rb:132:in 'Fastlane::CLIToolsDistributor.take_off' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/gems/fastlane-2.231.1/bin/fastlane:23:in '<top (required)>' /opt/homebrew/Cellar/ruby/4.0.1/lib/ruby/4.0.0/rubygems.rb:303:in 'Kernel#load' /opt/homebrew/Cellar/ruby/4.0.1/lib/ruby/4.0.0/rubygems.rb:303:in 'Gem.activate_and_load_bin_path' /opt/homebrew/Cellar/fastlane/2.231.1/libexec/bin/fastlane:25:in '<main>' Error uploading ipa file: [Application Loader Error Output]: [ContentDelivery.Uploader.104B12FA0]
|
||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="58490.472877">
|
||||
|
||||
</testcase>
|
||||
|
||||
|
||||
@@ -1586,5 +1586,304 @@
|
||||
"rawDump": "Rohdaten",
|
||||
"cannotRetryMissingRecipient": "Wiederholung nicht möglich: Empfängerinformationen fehlen",
|
||||
"voiceUnavailable": "Sprachnachricht momentan nicht verfügbar",
|
||||
"requestingVoice": "Sprachnachricht wird angefordert"
|
||||
"requestingVoice": "Sprachnachricht wird angefordert",
|
||||
"device": "Gerät",
|
||||
"change": "Ändern",
|
||||
"wizardOverviewDescription": "Diese App vereint MeshCore-Nachrichten, SAR-Feldupdates, Karten und Gerätetools an einem Ort.",
|
||||
"wizardOverviewFeature1": "Sende Direkt-, Raum- und Kanalnachrichten aus dem Haupttab \"Nachrichten\".",
|
||||
"wizardOverviewFeature2": "Teile SAR-Markierungen, Kartenzeichnungen, Sprachclips und Bilder über das Mesh.",
|
||||
"wizardOverviewFeature3": "Verbinde dich per BLE oder TCP und verwalte dann das Begleitfunkgerät direkt in der App.",
|
||||
"wizardMessagingTitle": "Nachrichten und Feldberichte",
|
||||
"wizardMessagingDescription": "Nachrichten sind hier mehr als nur Klartext. Die App unterstützt bereits mehrere operative Datentypen und Übertragungsabläufe.",
|
||||
"wizardMessagingFeature1": "Sende Direktnachrichten, Raumbeiträge und Kanalverkehr aus einem einzigen Editor.",
|
||||
"wizardMessagingFeature2": "Erstelle SAR-Updates und wiederverwendbare SAR-Vorlagen für häufige Feldberichte.",
|
||||
"wizardMessagingFeature3": "Übertrage Sprachsitzungen und Bilder mit Fortschritts- und Airtime-Anzeigen in der Oberfläche.",
|
||||
"wizardConnectDeviceTitle": "Gerät verbinden",
|
||||
"wizardConnectDeviceDescription": "Verbinde dein MeshCore-Funkgerät, wähle einen Namen und wende ein Funkprofil an, bevor du fortfährst.",
|
||||
"wizardSetupBadge": "Einrichtung",
|
||||
"wizardOverviewBadge": "Überblick",
|
||||
"wizardConnectedToDevice": "Verbunden mit {deviceName}",
|
||||
"wizardNoDeviceConnected": "Noch kein Gerät verbunden",
|
||||
"wizardSkipForNow": "Vorerst überspringen",
|
||||
"wizardDeviceNameLabel": "Gerätename",
|
||||
"wizardDeviceNameHelp": "Dieser Name wird an andere MeshCore-Nutzer ausgesendet.",
|
||||
"wizardConfigRegionLabel": "Konfigurationsregion",
|
||||
"wizardConfigRegionHelp": "Verwendet die vollständige offizielle Liste der MeshCore-Voreinstellungen. Standard ist EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Stelle sicher, dass die ausgewählte Voreinstellung zu deinen lokalen Funkvorschriften passt.",
|
||||
"wizardPresetNote2": "Die Liste entspricht dem offiziellen Preset-Feed des MeshCore-Konfigurationstools.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) bleibt standardmäßig für das Onboarding ausgewählt.",
|
||||
"wizardSaving": "Wird gespeichert...",
|
||||
"wizardSaveAndContinue": "Speichern und fortfahren",
|
||||
"wizardEnterDeviceName": "Gib vor dem Fortfahren einen Gerätenamen ein.",
|
||||
"wizardDeviceSetupSaved": "{deviceName} mit {presetName} gespeichert.",
|
||||
"wizardNetworkTitle": "Kontakte, Räume und Repeater",
|
||||
"wizardNetworkDescription": "Der Kontakte-Tab organisiert das Netzwerk, das du entdeckst, und die Routen, die im Laufe der Zeit gelernt werden.",
|
||||
"wizardNetworkFeature1": "Prüfe Teammitglieder, Repeater, Räume, Kanäle und ausstehende Adverts in einer Liste.",
|
||||
"wizardNetworkFeature2": "Nutze Smart Ping, Raum-Login, gelernte Pfade und Pfad-Reset-Tools, wenn die Verbindung unübersichtlich wird.",
|
||||
"wizardNetworkFeature3": "Erstelle Kanäle und verwalte Netzwerkziele, ohne die App zu verlassen.",
|
||||
"wizardMapOpsTitle": "Karte, Spuren und geteilte Geometrie",
|
||||
"wizardMapOpsDescription": "Die Karte der App ist direkt mit Nachrichten, Tracking und SAR-Overlays verbunden, statt nur ein separater Viewer zu sein.",
|
||||
"wizardMapOpsFeature1": "Verfolge deine eigene Position, Teamstandorte und Bewegungsspuren auf der Karte.",
|
||||
"wizardMapOpsFeature2": "Öffne Zeichnungen aus Nachrichten, prüfe sie inline und entferne sie bei Bedarf von der Karte.",
|
||||
"wizardMapOpsFeature3": "Nutze Repeater-Kartenansichten und geteilte Overlays, um die Netzabdeckung im Feld zu verstehen.",
|
||||
"wizardToolsTitle": "Werkzeuge jenseits von Nachrichten",
|
||||
"wizardToolsDescription": "Es gibt hier mehr als nur die vier Haupttabs. Die App enthält außerdem Konfiguration, Diagnose und optionale Sensor-Workflows.",
|
||||
"wizardToolsFeature1": "Öffne die Gerätekonfiguration, um Funkparameter, Telemetrie, Sendeleistung und Begleitdetails zu ändern.",
|
||||
"wizardToolsFeature2": "Aktiviere den Sensoren-Tab, wenn du überwachte Sensor-Dashboards und schnelle Aktualisierungen möchtest.",
|
||||
"wizardToolsFeature3": "Nutze Paketprotokolle, Spektrumsuche und Entwicklerdiagnosen zur Fehlersuche im Mesh.",
|
||||
"contactInSensors": "In Sensoren",
|
||||
"contactAddToSensors": "Zu Sensoren hinzufügen",
|
||||
"contactSetPath": "Pfad festlegen",
|
||||
"contactAddedToSensors": "{contactName} zu Sensoren hinzugefügt",
|
||||
"contactFailedToClearRoute": "Pfad konnte nicht gelöscht werden: {error}",
|
||||
"contactRouteCleared": "Pfad gelöscht",
|
||||
"contactRouteSet": "Pfad gesetzt: {route}",
|
||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK-Zeitüberschreitung",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Nutzlast",
|
||||
"hops": "Hops",
|
||||
"hashSize": "Hash-Größe",
|
||||
"pathBytes": "Pfad-Bytes",
|
||||
"selectedPath": "Ausgewählter Pfad",
|
||||
"estimatedTx": "Geschätzte Sendung",
|
||||
"senderToReceipt": "Sender bis Empfang",
|
||||
"receivedCopies": "Empfangene Kopien",
|
||||
"retryCause": "Wiederholungsursache",
|
||||
"retryMode": "Wiederholungsmodus",
|
||||
"retryResult": "Wiederholungsergebnis",
|
||||
"lastRetry": "Letzter Versuch",
|
||||
"rxPackets": "RX-Pakete",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Rate",
|
||||
"window": "Fenster",
|
||||
"posttxDelay": "Sendeverzögerung",
|
||||
"bandpass": "Bandpass",
|
||||
"bandpassFilterVoice": "Bandpassfilter Sprache",
|
||||
"frequency": "Frequenz",
|
||||
"australia": "Australien",
|
||||
"australiaNarrow": "Australien (Schmal)",
|
||||
"australiaQld": "Australien: QLD",
|
||||
"australiaSaWa": "Australien: SA, WA",
|
||||
"newZealand": "Neuseeland",
|
||||
"newZealandNarrow": "Neuseeland (Schmal)",
|
||||
"switzerland": "Schweiz",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Tschechien (Schmal)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Langstrecke)",
|
||||
"euukDeprecated": "EU/UK (Veraltet)",
|
||||
"euukNarrow": "EU/UK (Schmal)",
|
||||
"usacanadaRecommended": "USA/Kanada (Empfohlen)",
|
||||
"vietnamDeprecated": "Vietnam (Veraltet)",
|
||||
"vietnamNarrow": "Vietnam (Schmal)",
|
||||
"active": "Aktiv",
|
||||
"addContact": "Kontakt hinzufügen",
|
||||
"all": "Alle",
|
||||
"autoResolve": "Automatisch auflösen",
|
||||
"clearAllLabel": "Alle löschen",
|
||||
"clearRelays": "Relais löschen",
|
||||
"clearFilters": "Filter löschen",
|
||||
"clearRoute": "Route löschen",
|
||||
"clearMessages": "Nachrichten löschen",
|
||||
"clearScale": "Maßstab löschen",
|
||||
"clearDiscoveries": "Entdeckungen löschen",
|
||||
"clearOnlineTraceDatabase": "Online-Trace-Datenbank löschen",
|
||||
"clearAllChannels": "Alle Kanäle löschen",
|
||||
"clearAllContacts": "Alle Kontakte löschen",
|
||||
"clearChannels": "Kanäle löschen",
|
||||
"clearContacts": "Kontakte löschen",
|
||||
"clearPathOnMaxRetry": "Pfad bei max. Wiederholung löschen",
|
||||
"create": "Erstellen",
|
||||
"custom": "Benutzerdefiniert",
|
||||
"defaultValue": "Standard",
|
||||
"duplicate": "Duplizieren",
|
||||
"editName": "Name bearbeiten",
|
||||
"open": "Öffnen",
|
||||
"paste": "Einfügen",
|
||||
"preview": "Vorschau",
|
||||
"remove": "Entfernen",
|
||||
"rename": "Umbenennen",
|
||||
"resolveAll": "Alle auflösen",
|
||||
"send": "Senden",
|
||||
"sendAnyway": "Trotzdem senden",
|
||||
"share": "Teilen",
|
||||
"shareContact": "Kontakt teilen",
|
||||
"trace": "Trace",
|
||||
"use": "Verwenden",
|
||||
"useSelectedFrequency": "Ausgewählte Frequenz verwenden",
|
||||
"discovery": "Erkennung",
|
||||
"discoverRepeaters": "Repeater entdecken",
|
||||
"discoverSensors": "Sensoren entdecken",
|
||||
"repeaterDiscoverySent": "Repeater-Erkennung gesendet",
|
||||
"sensorDiscoverySent": "Sensor-Erkennung gesendet",
|
||||
"clearedPendingDiscoveries": "Ausstehende Entdeckungen gelöscht.",
|
||||
"autoDiscovery": "Automatische Erkennung",
|
||||
"enableAutomaticAdding": "Automatisches Hinzufügen aktivieren",
|
||||
"autoaddRepeaters": "Repeater automatisch hinzufügen",
|
||||
"autoaddRoomServers": "Raumserver automatisch hinzufügen",
|
||||
"autoaddSensors": "Sensoren automatisch hinzufügen",
|
||||
"autoaddUsers": "Benutzer automatisch hinzufügen",
|
||||
"overwriteOldestWhenFull": "Älteste bei vollem Speicher überschreiben",
|
||||
"storage": "Speicher",
|
||||
"dangerZone": "Gefahrenbereich",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Favoriten",
|
||||
"sensors": "Sensoren",
|
||||
"others": "Andere",
|
||||
"gpsModule": "GPS-Modul",
|
||||
"liveTraffic": "Live-Verkehr",
|
||||
"repeatersMap": "Repeater-Karte",
|
||||
"spectrumScan": "Spektrum-Scan",
|
||||
"blePacketLogs": "BLE-Paketprotokolle",
|
||||
"onlineTraceDatabase": "Online-Trace-Datenbank",
|
||||
"routePathByteSize": "Pfad-Bytegröße",
|
||||
"messageNotifications": "Nachrichtenbenachrichtigungen",
|
||||
"sarAlerts": "SAR-Alarme",
|
||||
"discoveryNotifications": "Erkennungsbenachrichtigungen",
|
||||
"updateNotifications": "Updatebenachrichtigungen",
|
||||
"muteWhileAppIsOpen": "Stumm bei geöffneter App",
|
||||
"disableContacts": "Kontakte deaktivieren",
|
||||
"enableSensorsTab": "Sensoren-Tab aktivieren",
|
||||
"enableProfiles": "Profile aktivieren",
|
||||
"autoRouteRotation": "Automatische Routenrotation",
|
||||
"nearestRepeaterFallback": "Nächster Repeater als Fallback",
|
||||
"deleteAllStoredMessageHistory": "Gesamten Nachrichtenverlauf löschen",
|
||||
"messageFontSize": "Nachrichtenschriftgröße",
|
||||
"rotateMapWithHeading": "Karte mit Richtung drehen",
|
||||
"showMapDebugInfo": "Karten-Debug-Info anzeigen",
|
||||
"openMapInFullscreen": "Karte im Vollbild öffnen",
|
||||
"showSarMarkersLabel": "SAR-Markierungen anzeigen",
|
||||
"displaySarMarkersOnTheMainMap": "SAR-Markierungen auf der Hauptkarte anzeigen",
|
||||
"showAllContactTrailsLabel": "Alle Kontaktpfade anzeigen",
|
||||
"hideRepeatersOnMap": "Repeater auf Karte ausblenden",
|
||||
"setMapScale": "Kartenmaßstab einstellen",
|
||||
"customMapScaleSaved": "Benutzerdefinierter Kartenmaßstab gespeichert",
|
||||
"voiceBitrate": "Sprach-Bitrate",
|
||||
"voiceCompressor": "Sprachkompressor",
|
||||
"balancesQuietAndLoudSpeechLevels": "Gleicht leise und laute Sprache aus",
|
||||
"voiceLimiter": "Sprach-Limiter",
|
||||
"preventsClippingPeaksBeforeEncoding": "Verhindert Clipping-Spitzen vor der Kodierung",
|
||||
"micAutoGain": "Mikrofon-Automatik",
|
||||
"letsTheRecorderAdjustInputLevel": "Lässt den Rekorder den Eingangspegel anpassen",
|
||||
"echoCancellation": "Echounterdrückung",
|
||||
"noiseSuppression": "Rauschunterdrückung",
|
||||
"trimSilenceInVoiceMessages": "Stille in Sprachnachrichten trimmen",
|
||||
"compressor": "Kompressor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatische Verstärkung",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Rauschen",
|
||||
"silenceTrim": "Stille",
|
||||
"maxImageSize": "Maximale Bildgröße",
|
||||
"imageCompression": "Bildkomprimierung",
|
||||
"grayscale": "Graustufen",
|
||||
"ultraMode": "Ultra-Modus",
|
||||
"fastPrivateGpsUpdates": "Schnelle private GPS-Updates",
|
||||
"movementThreshold": "Bewegungsschwelle",
|
||||
"fastGpsMovementThreshold": "Schneller GPS-Bewegungsschwellenwert",
|
||||
"fastGpsActiveuseInterval": "Schnelles GPS aktive Nutzung Intervall",
|
||||
"activeuseUpdateInterval": "Aktive Nutzung Updateintervall",
|
||||
"repeatNearbyTraffic": "Nahverkehr wiederholen",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Über Repeater im Mesh weiterleiten",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Nur in der Nähe, ohne Repeater-Flooding",
|
||||
"multihop": "Multi-Hop",
|
||||
"createProfile": "Profil erstellen",
|
||||
"renameProfile": "Profil umbenennen",
|
||||
"newProfile": "Neues Profil",
|
||||
"manageProfiles": "Profile verwalten",
|
||||
"enableProfilesToStartManagingThem": "Aktivieren Sie Profile, um sie zu verwalten.",
|
||||
"openMessage": "Nachricht öffnen",
|
||||
"jumpToTheRelatedSarMessage": "Zur zugehörigen SAR-Nachricht springen",
|
||||
"removeSarMarker": "SAR-Markierung entfernen",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Bitte wählen Sie ein Ziel zum Senden der SAR-Markierung",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-Markierung an öffentlichen Kanal gesendet",
|
||||
"sarMarkerSentToRoom": "SAR-Markierung an Raum gesendet",
|
||||
"loadFromGallery": "Aus Galerie laden",
|
||||
"replaceImage": "Bild ersetzen",
|
||||
"selectFromGallery": "Aus Galerie auswählen",
|
||||
"team": "Team",
|
||||
"found": "Gefunden",
|
||||
"staging": "Sammelpunkt",
|
||||
"object": "Objekt",
|
||||
"quiet": "Ruhig",
|
||||
"moderate": "Mäßig",
|
||||
"busy": "Beschäftigt",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum-Scan hat keine Kandidatenfrequenzen gefunden",
|
||||
"searchMessages": "Nachrichten suchen",
|
||||
"sendImageFromGallery": "Bild aus Galerie senden",
|
||||
"takePhoto": "Foto aufnehmen",
|
||||
"dmOnly": "Nur Direktnachricht",
|
||||
"allMessages": "Alle Nachrichten",
|
||||
"sendToPublicChannel": "An öffentlichen Kanal senden?",
|
||||
"selectMarkerTypeAndDestination": "Markierungstyp und Ziel auswählen",
|
||||
"noDestinationsAvailableLabel": "Keine Ziele verfügbar",
|
||||
"image": "Bild",
|
||||
"format": "Format",
|
||||
"dimensions": "Abmessungen",
|
||||
"segments": "Segmente",
|
||||
"transfers": "Übertragungen",
|
||||
"downloadedBy": "Heruntergeladen von",
|
||||
"saveDiscoverySettings": "Erkennungseinstellungen speichern",
|
||||
"savePublicInfo": "Öffentliche Info speichern",
|
||||
"saveRadioSettings": "Funkeinstellungen speichern",
|
||||
"savePath": "Pfad speichern",
|
||||
"wipeDeviceData": "Gerätedaten löschen",
|
||||
"wipeDevice": "Gerät löschen",
|
||||
"destructiveDeviceActions": "Destruktive Geräteaktionen.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wählen Sie eine Voreinstellung oder passen Sie die Funkeinstellungen an.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wählen Sie den Namen und Standort, den dieses Gerät teilt.",
|
||||
"availableSpaceOnThisDevice": "Verfügbarer Speicherplatz auf diesem Gerät.",
|
||||
"used": "Belegt",
|
||||
"total": "Gesamt",
|
||||
"renameValue": "Wert umbenennen",
|
||||
"customizeFields": "Felder anpassen",
|
||||
"livePreview": "Live-Vorschau",
|
||||
"refreshSchedule": "Aktualisierungsplan",
|
||||
"noResponse": "Keine Antwort",
|
||||
"refreshing": "Wird aktualisiert",
|
||||
"unavailable": "Nicht verfügbar",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wählen Sie einen Relay oder Knoten zur Beobachtung.",
|
||||
"publicKeyLabel": "Öffentlicher Schlüssel",
|
||||
"alreadyInContacts": "Bereits in Kontakten",
|
||||
"connectToADeviceBeforeAddingContacts": "Verbinden Sie sich mit einem Gerät, bevor Sie Kontakte hinzufügen",
|
||||
"fromContacts": "Aus Kontakten",
|
||||
"onlineOnly": "Nur online",
|
||||
"inBoth": "In beiden",
|
||||
"source": "Quelle",
|
||||
"manualRouteEdit": "Manuelle Routenbearbeitung",
|
||||
"observedMeshRoute": "Beobachtete Mesh-Route",
|
||||
"allMessagesCleared": "Alle Nachrichten gelöscht",
|
||||
"onlineTraceDatabaseCleared": "Online-Trace-Datenbank gelöscht",
|
||||
"packetLogsCleared": "Paketprotokolle gelöscht",
|
||||
"hexDataCopiedToClipboard": "Hex-Daten in die Zwischenablage kopiert",
|
||||
"developerModeEnabled": "Entwicklermodus aktiviert",
|
||||
"developerModeDisabled": "Entwicklermodus deaktiviert",
|
||||
"clipboardIsEmpty": "Zwischenablage ist leer",
|
||||
"contactImported": "Kontakt importiert",
|
||||
"contactLinkCopiedToClipboard": "Kontaktlink in Zwischenablage kopiert",
|
||||
"failedToExportContact": "Kontaktexport fehlgeschlagen",
|
||||
"noLogsToExport": "Keine Protokolle zum Exportieren",
|
||||
"exportAsCsv": "Als CSV exportieren",
|
||||
"exportAsText": "Als Text exportieren",
|
||||
"receivedRfc3339": "Empfangen (RFC3339)",
|
||||
"buildTime": "Build-Zeit",
|
||||
"downloadUrlNotAvailable": "Download-URL nicht verfügbar",
|
||||
"cannotOpenDownloadUrl": "Download-URL kann nicht geöffnet werden",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Update-Prüfung nur auf Android verfügbar",
|
||||
"youAreRunningTheLatestVersion": "Sie verwenden die neueste Version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Update verfügbar, aber Download-URL nicht gefunden",
|
||||
"startTictactoe": "Tic-Tac-Toe starten",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nicht verfügbar",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: Gegner unbekannt",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: Warten auf Start",
|
||||
"acceptsShareLinks": "Akzeptiert freigegebene Links",
|
||||
"supportsRawHex": "Unterstützt rohes Hex",
|
||||
"clipboardfriendly": "Zwischenablage-freundlich",
|
||||
"captured": "Erfasst",
|
||||
"size": "Größe",
|
||||
"noCustomChannelsToClear": "Keine benutzerdefinierten Kanäle zum Löschen.",
|
||||
"noDeviceContactsToClear": "Keine Gerätekontakte zum Löschen."
|
||||
}
|
||||
|
||||
@@ -2251,5 +2251,304 @@
|
||||
"rawDump": "Ακατέργαστα δεδομένα",
|
||||
"cannotRetryMissingRecipient": "Δεν είναι δυνατή η επανάληψη: λείπουν πληροφορίες παραλήπτη",
|
||||
"voiceUnavailable": "Η φωνή δεν είναι διαθέσιμη αυτή τη στιγμή",
|
||||
"requestingVoice": "Αίτηση φωνής"
|
||||
"requestingVoice": "Αίτηση φωνής",
|
||||
"device": "συσκευή",
|
||||
"change": "Αλλαγή",
|
||||
"wizardOverviewDescription": "Αυτή η εφαρμογή συνδυάζει μηνύματα MeshCore, ενημερώσεις πεδίου SAR, χαρτογράφηση και εργαλεία συσκευής σε ένα σημείο.",
|
||||
"wizardOverviewFeature1": "Στείλτε άμεσα μηνύματα, δημοσιεύσεις δωματίου και μηνύματα καναλιού από την κύρια καρτέλα Μηνύματα.",
|
||||
"wizardOverviewFeature2": "Μοιραστείτε δείκτες SAR, σχεδιάσεις χάρτη, ηχητικά αποσπάσματα και εικόνες μέσω του mesh.",
|
||||
"wizardOverviewFeature3": "Συνδεθείτε μέσω BLE ή TCP και στη συνέχεια διαχειριστείτε το συνοδευτικό ραδιόφωνο μέσα από την εφαρμογή.",
|
||||
"wizardMessagingTitle": "Μηνύματα και αναφορές πεδίου",
|
||||
"wizardMessagingDescription": "Τα μηνύματα εδώ είναι κάτι περισσότερο από απλό κείμενο. Η εφαρμογή υποστηρίζει ήδη πολλαπλά επιχειρησιακά φορτία και ροές μεταφοράς.",
|
||||
"wizardMessagingFeature1": "Στείλτε άμεσα μηνύματα, δημοσιεύσεις δωματίου και κίνηση καναλιού από έναν μόνο συντάκτη.",
|
||||
"wizardMessagingFeature2": "Δημιουργήστε ενημερώσεις SAR και επαναχρησιμοποιήσιμα πρότυπα SAR για συνηθισμένες αναφορές πεδίου.",
|
||||
"wizardMessagingFeature3": "Μεταφέρετε φωνητικές συνεδρίες και εικόνες, με ένδειξη προόδου και εκτιμήσεις χρόνου εκπομπής στο περιβάλλον.",
|
||||
"wizardConnectDeviceTitle": "Σύνδεση συσκευής",
|
||||
"wizardConnectDeviceDescription": "Συνδέστε το ραδιόφωνο MeshCore, επιλέξτε όνομα και εφαρμόστε μια προρύθμιση ραδιοφώνου πριν συνεχίσετε.",
|
||||
"wizardSetupBadge": "Ρύθμιση",
|
||||
"wizardOverviewBadge": "Επισκόπηση",
|
||||
"wizardConnectedToDevice": "Συνδέθηκε με το {deviceName}",
|
||||
"wizardNoDeviceConnected": "Δεν έχει συνδεθεί ακόμη συσκευή",
|
||||
"wizardSkipForNow": "Παράλειψη προς το παρόν",
|
||||
"wizardDeviceNameLabel": "Όνομα συσκευής",
|
||||
"wizardDeviceNameHelp": "Αυτό το όνομα ανακοινώνεται σε άλλους χρήστες του MeshCore.",
|
||||
"wizardConfigRegionLabel": "Περιοχή ρύθμισης",
|
||||
"wizardConfigRegionHelp": "Χρησιμοποιεί την πλήρη επίσημη λίστα προρυθμίσεων MeshCore. Η προεπιλογή είναι EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Βεβαιωθείτε ότι η επιλεγμένη προρύθμιση ταιριάζει με τους τοπικούς ραδιοφωνικούς κανονισμούς.",
|
||||
"wizardPresetNote2": "Η λίστα αντιστοιχεί στην επίσημη ροή προρυθμίσεων του εργαλείου ρύθμισης MeshCore.",
|
||||
"wizardPresetNote3": "Το EU/UK (Narrow) παραμένει επιλεγμένο από προεπιλογή κατά την αρχική ρύθμιση.",
|
||||
"wizardSaving": "Αποθήκευση...",
|
||||
"wizardSaveAndContinue": "Αποθήκευση και συνέχεια",
|
||||
"wizardEnterDeviceName": "Εισαγάγετε όνομα συσκευής πριν συνεχίσετε.",
|
||||
"wizardDeviceSetupSaved": "Αποθηκεύτηκε το {deviceName} με {presetName}.",
|
||||
"wizardNetworkTitle": "Επαφές, δωμάτια και αναμεταδότες",
|
||||
"wizardNetworkDescription": "Η καρτέλα Επαφές οργανώνει το δίκτυο που ανακαλύπτετε και τις διαδρομές που μαθαίνετε με τον χρόνο.",
|
||||
"wizardNetworkFeature1": "Ελέγξτε μέλη ομάδας, αναμεταδότες, δωμάτια, κανάλια και εκκρεμείς ανακοινώσεις σε μία λίστα.",
|
||||
"wizardNetworkFeature2": "Χρησιμοποιήστε smart ping, σύνδεση σε δωμάτια, μαθημένες διαδρομές και εργαλεία επαναφοράς διαδρομών όταν η συνδεσιμότητα γίνεται χαοτική.",
|
||||
"wizardNetworkFeature3": "Δημιουργήστε κανάλια και διαχειριστείτε προορισμούς δικτύου χωρίς να φύγετε από την εφαρμογή.",
|
||||
"wizardMapOpsTitle": "Χάρτης, ίχνη και κοινόχρηστη γεωμετρία",
|
||||
"wizardMapOpsDescription": "Ο χάρτης της εφαρμογής συνδέεται άμεσα με τα μηνύματα, την παρακολούθηση και τα επικαλύμματα SAR αντί να είναι ξεχωριστός προβολέας.",
|
||||
"wizardMapOpsFeature1": "Παρακολουθήστε τη θέση σας, τις τοποθεσίες της ομάδας και τα ίχνη κίνησης στον χάρτη.",
|
||||
"wizardMapOpsFeature2": "Ανοίξτε σχεδιάσεις από μηνύματα, προεπισκοπήστε τες επιτόπου και αφαιρέστε τες από τον χάρτη όταν χρειάζεται.",
|
||||
"wizardMapOpsFeature3": "Χρησιμοποιήστε προβολές χάρτη αναμεταδοτών και κοινόχρηστα επικαλύμματα για να κατανοήσετε την εμβέλεια του δικτύου στο πεδίο.",
|
||||
"wizardToolsTitle": "Εργαλεία πέρα από τα μηνύματα",
|
||||
"wizardToolsDescription": "Υπάρχουν περισσότερα εδώ από τις τέσσερις κύριες καρτέλες. Η εφαρμογή περιλαμβάνει επίσης ρυθμίσεις, διαγνωστικά και προαιρετικές ροές αισθητήρων.",
|
||||
"wizardToolsFeature1": "Ανοίξτε τη ρύθμιση συσκευής για να αλλάξετε παραμέτρους ραδιοφώνου, τηλεμετρία, ισχύ TX και στοιχεία συνοδευτικής συσκευής.",
|
||||
"wizardToolsFeature2": "Ενεργοποιήστε την καρτέλα Αισθητήρες όταν θέλετε πίνακες παρακολούθησης και γρήγορες ενέργειες ανανέωσης.",
|
||||
"wizardToolsFeature3": "Χρησιμοποιήστε καταγραφές πακέτων, σάρωση φάσματος και διαγνωστικά προγραμματιστή κατά την αντιμετώπιση προβλημάτων του mesh.",
|
||||
"contactInSensors": "Στους αισθητήρες",
|
||||
"contactAddToSensors": "Προσθήκη στους αισθητήρες",
|
||||
"contactSetPath": "Ορισμός διαδρομής",
|
||||
"contactAddedToSensors": "Το {contactName} προστέθηκε στους Αισθητήρες",
|
||||
"contactFailedToClearRoute": "Αποτυχία εκκαθάρισης διαδρομής: {error}",
|
||||
"contactRouteCleared": "Η διαδρομή εκκαθαρίστηκε",
|
||||
"contactRouteSet": "Ορίστηκε διαδρομή: {route}",
|
||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Λήξη χρόνου ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ωφέλιμο φορτίο",
|
||||
"hops": "Αναπηδήσεις",
|
||||
"hashSize": "Μέγεθος hash",
|
||||
"pathBytes": "Bytes διαδρομής",
|
||||
"selectedPath": "Επιλεγμένη διαδρομή",
|
||||
"estimatedTx": "Εκτιμώμενη εκπομπή",
|
||||
"senderToReceipt": "Αποστολέας σε απόδειξη",
|
||||
"receivedCopies": "Ληφθέντα αντίγραφα",
|
||||
"retryCause": "Αιτία επανάληψης",
|
||||
"retryMode": "Λειτουργία επανάληψης",
|
||||
"retryResult": "Αποτέλεσμα επανάληψης",
|
||||
"lastRetry": "Τελευταία επανάληψη",
|
||||
"rxPackets": "Πακέτα RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Ρυθμός",
|
||||
"window": "Παράθυρο",
|
||||
"posttxDelay": "Καθυστέρηση μετά-tx",
|
||||
"bandpass": "Ζωνοπερατό",
|
||||
"bandpassFilterVoice": "Ζωνοπερατό φίλτρο φωνής",
|
||||
"frequency": "Συχνότητα",
|
||||
"australia": "Αυστραλία",
|
||||
"australiaNarrow": "Αυστραλία (Στενό)",
|
||||
"australiaQld": "Αυστραλία: QLD",
|
||||
"australiaSaWa": "Αυστραλία: SA, WA",
|
||||
"newZealand": "Νέα Ζηλανδία",
|
||||
"newZealandNarrow": "Νέα Ζηλανδία (Στενό)",
|
||||
"switzerland": "Ελβετία",
|
||||
"portugal433": "Πορτογαλία 433",
|
||||
"portugal868": "Πορτογαλία 868",
|
||||
"czechRepublicNarrow": "Τσεχία (Στενό)",
|
||||
"eu433mhzLongRange": "ΕΕ 433MHz (Μεγάλη Εμβέλεια)",
|
||||
"euukDeprecated": "ΕΕ/ΗΒ (Παρωχημένο)",
|
||||
"euukNarrow": "ΕΕ/ΗΒ (Στενό)",
|
||||
"usacanadaRecommended": "ΗΠΑ/Καναδάς (Προτεινόμενο)",
|
||||
"vietnamDeprecated": "Βιετνάμ (Παρωχημένο)",
|
||||
"vietnamNarrow": "Βιετνάμ (Στενό)",
|
||||
"active": "Ενεργό",
|
||||
"addContact": "Προσθήκη επαφής",
|
||||
"all": "Όλα",
|
||||
"autoResolve": "Αυτόματη επίλυση",
|
||||
"clearAllLabel": "Εκκαθάριση όλων",
|
||||
"clearRelays": "Εκκαθάριση αναμεταδοτών",
|
||||
"clearFilters": "Εκκαθάριση φίλτρων",
|
||||
"clearRoute": "Εκκαθάριση διαδρομής",
|
||||
"clearMessages": "Εκκαθάριση μηνυμάτων",
|
||||
"clearScale": "Εκκαθάριση κλίμακας",
|
||||
"clearDiscoveries": "Εκκαθάριση ανακαλύψεων",
|
||||
"clearOnlineTraceDatabase": "Εκκαθάριση βάσης ιχνών",
|
||||
"clearAllChannels": "Εκκαθάριση όλων των καναλιών",
|
||||
"clearAllContacts": "Εκκαθάριση όλων των επαφών",
|
||||
"clearChannels": "Εκκαθάριση καναλιών",
|
||||
"clearContacts": "Εκκαθάριση επαφών",
|
||||
"clearPathOnMaxRetry": "Εκκαθάριση μονοπατιού στο μέγιστο",
|
||||
"create": "Δημιουργία",
|
||||
"custom": "Προσαρμοσμένο",
|
||||
"defaultValue": "Προεπιλογή",
|
||||
"duplicate": "Αντιγραφή",
|
||||
"editName": "Επεξεργασία ονόματος",
|
||||
"open": "Άνοιγμα",
|
||||
"paste": "Επικόλληση",
|
||||
"preview": "Προεπισκόπηση",
|
||||
"remove": "Αφαίρεση",
|
||||
"rename": "Μετονομασία",
|
||||
"resolveAll": "Επίλυση όλων",
|
||||
"send": "Αποστολή",
|
||||
"sendAnyway": "Αποστολή ούτως ή άλλως",
|
||||
"share": "Κοινοποίηση",
|
||||
"shareContact": "Κοινοποίηση επαφής",
|
||||
"trace": "Ίχνος",
|
||||
"use": "Χρήση",
|
||||
"useSelectedFrequency": "Χρήση επιλεγμένης συχνότητας",
|
||||
"discovery": "Ανακάλυψη",
|
||||
"discoverRepeaters": "Ανακάλυψη αναμεταδοτών",
|
||||
"discoverSensors": "Ανακάλυψη αισθητήρων",
|
||||
"repeaterDiscoverySent": "Αποστολή ανακάλυψης αναμεταδοτών",
|
||||
"sensorDiscoverySent": "Αποστολή ανακάλυψης αισθητήρων",
|
||||
"clearedPendingDiscoveries": "Εκκαθάριση εκκρεμών ανακαλύψεων.",
|
||||
"autoDiscovery": "Αυτόματη ανακάλυψη",
|
||||
"enableAutomaticAdding": "Ενεργοποίηση αυτόματης προσθήκης",
|
||||
"autoaddRepeaters": "Αυτόματη προσθήκη αναμεταδοτών",
|
||||
"autoaddRoomServers": "Αυτόματη προσθήκη διακομιστών δωματίου",
|
||||
"autoaddSensors": "Αυτόματη προσθήκη αισθητήρων",
|
||||
"autoaddUsers": "Αυτόματη προσθήκη χρηστών",
|
||||
"overwriteOldestWhenFull": "Αντικατάσταση παλαιότερων όταν γεμίσει",
|
||||
"storage": "Αποθήκευση",
|
||||
"dangerZone": "Ζώνη κινδύνου",
|
||||
"profiles": "Προφίλ",
|
||||
"favourites": "Αγαπημένα",
|
||||
"sensors": "Αισθητήρες",
|
||||
"others": "Άλλοι",
|
||||
"gpsModule": "Μονάδα GPS",
|
||||
"liveTraffic": "Ζωντανή κίνηση",
|
||||
"repeatersMap": "Χάρτης αναμεταδοτών",
|
||||
"spectrumScan": "Σάρωση φάσματος",
|
||||
"blePacketLogs": "Αρχεία πακέτων BLE",
|
||||
"onlineTraceDatabase": "Βάση δεδομένων ιχνών",
|
||||
"routePathByteSize": "Μέγεθος διαδρομής σε bytes",
|
||||
"messageNotifications": "Ειδοποιήσεις μηνυμάτων",
|
||||
"sarAlerts": "Ειδοποιήσεις SAR",
|
||||
"discoveryNotifications": "Ειδοποιήσεις ανακάλυψης",
|
||||
"updateNotifications": "Ειδοποιήσεις ενημερώσεων",
|
||||
"muteWhileAppIsOpen": "Σίγαση με ανοιχτή εφαρμογή",
|
||||
"disableContacts": "Απενεργοποίηση επαφών",
|
||||
"enableSensorsTab": "Ενεργοποίηση καρτέλας Αισθητήρων",
|
||||
"enableProfiles": "Ενεργοποίηση προφίλ",
|
||||
"autoRouteRotation": "Αυτόματη εναλλαγή διαδρομής",
|
||||
"nearestRepeaterFallback": "Πλησιέστερος αναμεταδότης ως εφεδρεία",
|
||||
"deleteAllStoredMessageHistory": "Διαγραφή όλου του ιστορικού",
|
||||
"messageFontSize": "Μέγεθος γραμματοσειράς μηνυμάτων",
|
||||
"rotateMapWithHeading": "Περιστροφή χάρτη με κατεύθυνση",
|
||||
"showMapDebugInfo": "Εμφάνιση πληροφοριών αποσφαλμάτωσης",
|
||||
"openMapInFullscreen": "Άνοιγμα χάρτη σε πλήρη οθόνη",
|
||||
"showSarMarkersLabel": "Εμφάνιση δεικτών SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Εμφάνιση δεικτών SAR στον κύριο χάρτη",
|
||||
"showAllContactTrailsLabel": "Εμφάνιση όλων των ιχνών επαφών",
|
||||
"hideRepeatersOnMap": "Απόκρυψη αναμεταδοτών στον χάρτη",
|
||||
"setMapScale": "Ρύθμιση κλίμακας χάρτη",
|
||||
"customMapScaleSaved": "Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε",
|
||||
"voiceBitrate": "Ρυθμός bit φωνής",
|
||||
"voiceCompressor": "Συμπιεστής φωνής",
|
||||
"balancesQuietAndLoudSpeechLevels": "Εξισορρόπηση χαμηλών και δυνατών επιπέδων",
|
||||
"voiceLimiter": "Περιοριστής φωνής",
|
||||
"preventsClippingPeaksBeforeEncoding": "Αποτρέπει κοπή κορυφών πριν την κωδικοποίηση",
|
||||
"micAutoGain": "Αυτόματη ενίσχυση μικροφώνου",
|
||||
"letsTheRecorderAdjustInputLevel": "Αφήνει τη συσκευή να ρυθμίσει την ένταση",
|
||||
"echoCancellation": "Ακύρωση ηχούς",
|
||||
"noiseSuppression": "Καταστολή θορύβου",
|
||||
"trimSilenceInVoiceMessages": "Περικοπή σιωπής σε φωνητικά μηνύματα",
|
||||
"compressor": "Συμπιεστής",
|
||||
"limiter": "Περιοριστής",
|
||||
"autoGain": "Αυτόματη ενίσχυση",
|
||||
"echoCancel": "Ηχώ",
|
||||
"noiseSuppress": "Θόρυβος",
|
||||
"silenceTrim": "Σιωπή",
|
||||
"maxImageSize": "Μέγιστο μέγεθος εικόνας",
|
||||
"imageCompression": "Συμπίεση εικόνας",
|
||||
"grayscale": "Κλίμακα του γκρι",
|
||||
"ultraMode": "Λειτουργία ultra",
|
||||
"fastPrivateGpsUpdates": "Γρήγορες ιδιωτικές ενημερώσεις GPS",
|
||||
"movementThreshold": "Κατώφλι κίνησης",
|
||||
"fastGpsMovementThreshold": "Κατώφλι κίνησης γρήγορου GPS",
|
||||
"fastGpsActiveuseInterval": "Διάστημα ενεργής χρήσης γρήγορου GPS",
|
||||
"activeuseUpdateInterval": "Διάστημα ενημέρωσης ενεργής χρήσης",
|
||||
"repeatNearbyTraffic": "Επανάληψη κοντινής κίνησης",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Αναμετάδοση μέσω αναμεταδοτών στο δίκτυο",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Μόνο κοντά, χωρίς πλημμυρισμό",
|
||||
"multihop": "Πολλαπλών αναπηδήσεων",
|
||||
"createProfile": "Δημιουργία προφίλ",
|
||||
"renameProfile": "Μετονομασία προφίλ",
|
||||
"newProfile": "Νέο προφίλ",
|
||||
"manageProfiles": "Διαχείριση προφίλ",
|
||||
"enableProfilesToStartManagingThem": "Ενεργοποιήστε τα προφίλ για να τα διαχειριστείτε.",
|
||||
"openMessage": "Άνοιγμα μηνύματος",
|
||||
"jumpToTheRelatedSarMessage": "Μετάβαση στο σχετικό μήνυμα SAR",
|
||||
"removeSarMarker": "Αφαίρεση δείκτη SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Επιλέξτε προορισμό για αποστολή δείκτη SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Δείκτης SAR εκπεμπόμενος στο δημόσιο κανάλι",
|
||||
"sarMarkerSentToRoom": "Δείκτης SAR εστάλη στο δωμάτιο",
|
||||
"loadFromGallery": "Φόρτωση από τη γκαλερί",
|
||||
"replaceImage": "Αντικατάσταση εικόνας",
|
||||
"selectFromGallery": "Επιλογή από τη γκαλερί",
|
||||
"team": "Ομάδα",
|
||||
"found": "Βρέθηκε",
|
||||
"staging": "Χώρος συγκέντρωσης",
|
||||
"object": "Αντικείμενο",
|
||||
"quiet": "Ήσυχο",
|
||||
"moderate": "Μέτριο",
|
||||
"busy": "Απασχολημένο",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Η σάρωση φάσματος δεν βρήκε υποψήφιες συχνότητες",
|
||||
"searchMessages": "Αναζήτηση μηνυμάτων",
|
||||
"sendImageFromGallery": "Αποστολή εικόνας από τη γκαλερί",
|
||||
"takePhoto": "Λήψη φωτογραφίας",
|
||||
"dmOnly": "Μόνο απευθείας",
|
||||
"allMessages": "Όλα τα μηνύματα",
|
||||
"sendToPublicChannel": "Αποστολή στο δημόσιο κανάλι;",
|
||||
"selectMarkerTypeAndDestination": "Επιλέξτε τύπο δείκτη και προορισμό",
|
||||
"noDestinationsAvailableLabel": "Δεν υπάρχουν διαθέσιμοι προορισμοί",
|
||||
"image": "Εικόνα",
|
||||
"format": "Μορφή",
|
||||
"dimensions": "Διαστάσεις",
|
||||
"segments": "Τμήματα",
|
||||
"transfers": "Μεταφορές",
|
||||
"downloadedBy": "Λήφθηκε από",
|
||||
"saveDiscoverySettings": "Αποθήκευση ρυθμίσεων ανακάλυψης",
|
||||
"savePublicInfo": "Αποθήκευση δημόσιων πληροφοριών",
|
||||
"saveRadioSettings": "Αποθήκευση ρυθμίσεων ραδιοφώνου",
|
||||
"savePath": "Αποθήκευση διαδρομής",
|
||||
"wipeDeviceData": "Διαγραφή δεδομένων συσκευής",
|
||||
"wipeDevice": "Διαγραφή συσκευής",
|
||||
"destructiveDeviceActions": "Καταστροφικές ενέργειες συσκευής.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Επιλέξτε το όνομα και την τοποθεσία που μοιράζεται αυτή η συσκευή.",
|
||||
"availableSpaceOnThisDevice": "Διαθέσιμος χώρος σε αυτή τη συσκευή.",
|
||||
"used": "Χρησιμοποιήθηκε",
|
||||
"total": "Σύνολο",
|
||||
"renameValue": "Μετονομασία τιμής",
|
||||
"customizeFields": "Προσαρμογή πεδίων",
|
||||
"livePreview": "Ζωντανή προεπισκόπηση",
|
||||
"refreshSchedule": "Χρονοδιάγραμμα ανανέωσης",
|
||||
"noResponse": "Χωρίς απάντηση",
|
||||
"refreshing": "Ανανέωση",
|
||||
"unavailable": "Μη διαθέσιμο",
|
||||
"pickARelayOrNodeToWatchInSensors": "Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.",
|
||||
"publicKeyLabel": "Δημόσιο κλειδί",
|
||||
"alreadyInContacts": "Ήδη στις επαφές",
|
||||
"connectToADeviceBeforeAddingContacts": "Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές",
|
||||
"fromContacts": "Από τις επαφές",
|
||||
"onlineOnly": "Μόνο συνδεδεμένοι",
|
||||
"inBoth": "Σε αμφότερα",
|
||||
"source": "Πηγή",
|
||||
"manualRouteEdit": "Χειροκίνητη επεξεργασία διαδρομής",
|
||||
"observedMeshRoute": "Παρατηρούμενη διαδρομή δικτύου",
|
||||
"allMessagesCleared": "Όλα τα μηνύματα διαγράφηκαν",
|
||||
"onlineTraceDatabaseCleared": "Βάση δεδομένων ιχνών εκκαθαρίστηκε",
|
||||
"packetLogsCleared": "Τα αρχεία πακέτων εκκαθαρίστηκαν",
|
||||
"hexDataCopiedToClipboard": "Τα δεδομένα hex αντιγράφηκαν",
|
||||
"developerModeEnabled": "Λειτουργία προγραμματιστή ενεργοποιήθηκε",
|
||||
"developerModeDisabled": "Λειτουργία προγραμματιστή απενεργοποιήθηκε",
|
||||
"clipboardIsEmpty": "Το πρόχειρο είναι κενό",
|
||||
"contactImported": "Επαφή εισήχθη",
|
||||
"contactLinkCopiedToClipboard": "Ο σύνδεσμος επαφής αντιγράφηκε",
|
||||
"failedToExportContact": "Αποτυχία εξαγωγής επαφής",
|
||||
"noLogsToExport": "Δεν υπάρχουν αρχεία για εξαγωγή",
|
||||
"exportAsCsv": "Εξαγωγή ως CSV",
|
||||
"exportAsText": "Εξαγωγή ως κείμενο",
|
||||
"receivedRfc3339": "Ελήφθη (RFC3339)",
|
||||
"buildTime": "Ώρα κατασκευής",
|
||||
"downloadUrlNotAvailable": "Η URL λήψης δεν είναι διαθέσιμη",
|
||||
"cannotOpenDownloadUrl": "Αδυναμία ανοίγματος URL λήψης",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Ο έλεγχος ενημερώσεων είναι διαθέσιμος μόνο σε Android",
|
||||
"youAreRunningTheLatestVersion": "Χρησιμοποιείτε την πιο πρόσφατη έκδοση",
|
||||
"updateAvailableButDownloadUrlNotFound": "Διαθέσιμη ενημέρωση αλλά η URL λήψης δεν βρέθηκε",
|
||||
"startTictactoe": "Εκκίνηση Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe μη διαθέσιμο",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: άγνωστος αντίπαλος",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: αναμονή για εκκίνηση",
|
||||
"acceptsShareLinks": "Αποδέχεται κοινόχρηστους συνδέσμους",
|
||||
"supportsRawHex": "Υποστηρίζει ακατέργαστο hex",
|
||||
"clipboardfriendly": "Φιλικό προς πρόχειρο",
|
||||
"captured": "Καταγράφηκε",
|
||||
"size": "Μέγεθος",
|
||||
"noCustomChannelsToClear": "Δεν υπάρχουν προσαρμοσμένα κανάλια.",
|
||||
"noDeviceContactsToClear": "Δεν υπάρχουν επαφές συσκευής."
|
||||
}
|
||||
|
||||
@@ -2228,78 +2228,547 @@
|
||||
"description": "Message when there are no chat contacts to send to"
|
||||
},
|
||||
"reply": "Reply",
|
||||
"@reply": {
|
||||
},
|
||||
"@reply": {},
|
||||
"technicalDetails": "Technical details",
|
||||
"@technicalDetails": {
|
||||
},
|
||||
"@technicalDetails": {},
|
||||
"messageTechnicalDetails": "Message technical details",
|
||||
"@messageTechnicalDetails": {
|
||||
},
|
||||
"@messageTechnicalDetails": {},
|
||||
"linkQuality": "Link quality",
|
||||
"@linkQuality": {
|
||||
},
|
||||
"@linkQuality": {},
|
||||
"delivery": "Delivery",
|
||||
"@delivery": {
|
||||
},
|
||||
"@delivery": {},
|
||||
"status": "Status",
|
||||
"@status": {
|
||||
},
|
||||
"@status": {},
|
||||
"expectedAckTag": "Expected ACK tag",
|
||||
"@expectedAckTag": {
|
||||
},
|
||||
"@expectedAckTag": {},
|
||||
"roundTrip": "Round-trip",
|
||||
"@roundTrip": {
|
||||
},
|
||||
"@roundTrip": {},
|
||||
"retryAttempt": "Retry attempt",
|
||||
"@retryAttempt": {
|
||||
},
|
||||
"@retryAttempt": {},
|
||||
"floodFallback": "Flood fallback",
|
||||
"@floodFallback": {
|
||||
},
|
||||
"@floodFallback": {},
|
||||
"identity": "Identity",
|
||||
"@identity": {
|
||||
},
|
||||
"@identity": {},
|
||||
"messageId": "Message ID",
|
||||
"@messageId": {
|
||||
},
|
||||
"@messageId": {},
|
||||
"sender": "Sender",
|
||||
"@sender": {
|
||||
},
|
||||
"@sender": {},
|
||||
"senderKey": "Sender key",
|
||||
"@senderKey": {
|
||||
},
|
||||
"@senderKey": {},
|
||||
"recipient": "Recipient",
|
||||
"@recipient": {
|
||||
},
|
||||
"@recipient": {},
|
||||
"recipientKey": "Recipient key",
|
||||
"@recipientKey": {
|
||||
},
|
||||
"@recipientKey": {},
|
||||
"voice": "Voice",
|
||||
"@voice": {
|
||||
},
|
||||
"@voice": {},
|
||||
"voiceId": "Voice ID",
|
||||
"@voiceId": {
|
||||
},
|
||||
"@voiceId": {},
|
||||
"envelope": "Envelope",
|
||||
"@envelope": {
|
||||
},
|
||||
"@envelope": {},
|
||||
"sessionProgress": "Session progress",
|
||||
"@sessionProgress": {
|
||||
},
|
||||
"@sessionProgress": {},
|
||||
"complete": "Complete",
|
||||
"@complete": {
|
||||
},
|
||||
"@complete": {},
|
||||
"rawDump": "Raw dump",
|
||||
"@rawDump": {
|
||||
},
|
||||
"@rawDump": {},
|
||||
"cannotRetryMissingRecipient": "Cannot retry: recipient information missing",
|
||||
"@cannotRetryMissingRecipient": {
|
||||
},
|
||||
"@cannotRetryMissingRecipient": {},
|
||||
"voiceUnavailable": "Voice unavailable right now",
|
||||
"@voiceUnavailable": {
|
||||
},
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Requesting voice",
|
||||
"@requestingVoice": {
|
||||
}
|
||||
"@requestingVoice": {},
|
||||
"device": "device",
|
||||
"@device": {
|
||||
"description": "Generic device label"
|
||||
},
|
||||
"change": "Change",
|
||||
"@change": {
|
||||
"description": "Generic change action"
|
||||
},
|
||||
"wizardOverviewDescription": "This app combines MeshCore messaging, SAR field updates, mapping, and device tools in one place.",
|
||||
"@wizardOverviewDescription": {
|
||||
"description": "Updated onboarding overview description"
|
||||
},
|
||||
"wizardOverviewFeature1": "Send direct, room, and channel messages from the main Messages tab.",
|
||||
"@wizardOverviewFeature1": {
|
||||
"description": "Updated onboarding overview feature 1"
|
||||
},
|
||||
"wizardOverviewFeature2": "Share SAR markers, map drawings, voice clips, and images over the mesh.",
|
||||
"@wizardOverviewFeature2": {
|
||||
"description": "Updated onboarding overview feature 2"
|
||||
},
|
||||
"wizardOverviewFeature3": "Connect over BLE or TCP, then manage the companion radio from inside the app.",
|
||||
"@wizardOverviewFeature3": {
|
||||
"description": "Updated onboarding overview feature 3"
|
||||
},
|
||||
"wizardMessagingTitle": "Messaging and Field Reports",
|
||||
"@wizardMessagingTitle": {
|
||||
"description": "Updated onboarding messaging page title"
|
||||
},
|
||||
"wizardMessagingDescription": "Messages are more than plain text here. The app already supports several operational payloads and transfer workflows.",
|
||||
"@wizardMessagingDescription": {
|
||||
"description": "Updated onboarding messaging page description"
|
||||
},
|
||||
"wizardMessagingFeature1": "Send direct messages, room posts, and channel traffic from one composer.",
|
||||
"@wizardMessagingFeature1": {
|
||||
"description": "Updated onboarding messaging feature 1"
|
||||
},
|
||||
"wizardMessagingFeature2": "Create SAR updates and reusable SAR templates for common field reports.",
|
||||
"@wizardMessagingFeature2": {
|
||||
"description": "Updated onboarding messaging feature 2"
|
||||
},
|
||||
"wizardMessagingFeature3": "Transfer voice sessions and images, with progress and airtime estimates in the UI.",
|
||||
"@wizardMessagingFeature3": {
|
||||
"description": "Updated onboarding messaging feature 3"
|
||||
},
|
||||
"wizardConnectDeviceTitle": "Connect device",
|
||||
"@wizardConnectDeviceTitle": {
|
||||
"description": "Onboarding connect device page title"
|
||||
},
|
||||
"wizardConnectDeviceDescription": "Connect your MeshCore radio, choose a name, and apply a radio preset before continuing.",
|
||||
"@wizardConnectDeviceDescription": {
|
||||
"description": "Onboarding connect device page description"
|
||||
},
|
||||
"wizardSetupBadge": "Setup",
|
||||
"@wizardSetupBadge": {
|
||||
"description": "Onboarding setup badge label"
|
||||
},
|
||||
"wizardOverviewBadge": "Overview",
|
||||
"@wizardOverviewBadge": {
|
||||
"description": "Onboarding overview badge label"
|
||||
},
|
||||
"wizardConnectedToDevice": "Connected to {deviceName}",
|
||||
"@wizardConnectedToDevice": {
|
||||
"description": "Onboarding connection status when connected",
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wizardNoDeviceConnected": "No device connected yet",
|
||||
"@wizardNoDeviceConnected": {
|
||||
"description": "Onboarding connection status when disconnected"
|
||||
},
|
||||
"wizardSkipForNow": "Skip for now",
|
||||
"@wizardSkipForNow": {
|
||||
"description": "Onboarding skip current setup step action"
|
||||
},
|
||||
"wizardDeviceNameLabel": "Device name",
|
||||
"@wizardDeviceNameLabel": {
|
||||
"description": "Onboarding device name field label"
|
||||
},
|
||||
"wizardDeviceNameHelp": "This name is advertised to other MeshCore users.",
|
||||
"@wizardDeviceNameHelp": {
|
||||
"description": "Onboarding device name field helper text"
|
||||
},
|
||||
"wizardConfigRegionLabel": "Config region",
|
||||
"@wizardConfigRegionLabel": {
|
||||
"description": "Onboarding config region field label"
|
||||
},
|
||||
"wizardConfigRegionHelp": "Uses the full official MeshCore preset list. Default is EU/UK (Narrow).",
|
||||
"@wizardConfigRegionHelp": {
|
||||
"description": "Onboarding config region helper text"
|
||||
},
|
||||
"wizardPresetNote1": "Make sure the selected preset matches your local radio regulations.",
|
||||
"@wizardPresetNote1": {
|
||||
"description": "Onboarding preset note 1"
|
||||
},
|
||||
"wizardPresetNote2": "The list matches the official MeshCore config tool preset feed.",
|
||||
"@wizardPresetNote2": {
|
||||
"description": "Onboarding preset note 2"
|
||||
},
|
||||
"wizardPresetNote3": "EU/UK (Narrow) stays selected by default for onboarding.",
|
||||
"@wizardPresetNote3": {
|
||||
"description": "Onboarding preset note 3"
|
||||
},
|
||||
"wizardSaving": "Saving...",
|
||||
"@wizardSaving": {
|
||||
"description": "Onboarding saving button label"
|
||||
},
|
||||
"wizardSaveAndContinue": "Save and continue",
|
||||
"@wizardSaveAndContinue": {
|
||||
"description": "Onboarding save and continue button label"
|
||||
},
|
||||
"wizardEnterDeviceName": "Enter a device name before continuing.",
|
||||
"@wizardEnterDeviceName": {
|
||||
"description": "Onboarding validation when device name is missing"
|
||||
},
|
||||
"wizardDeviceSetupSaved": "Saved {deviceName} with {presetName}.",
|
||||
"@wizardDeviceSetupSaved": {
|
||||
"description": "Onboarding success message after saving device setup",
|
||||
"placeholders": {
|
||||
"deviceName": {
|
||||
"type": "String"
|
||||
},
|
||||
"presetName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"wizardNetworkTitle": "Contacts, Rooms, and Repeaters",
|
||||
"@wizardNetworkTitle": {
|
||||
"description": "Updated onboarding network page title"
|
||||
},
|
||||
"wizardNetworkDescription": "The Contacts tab organizes the network you discover and the routes you learn over time.",
|
||||
"@wizardNetworkDescription": {
|
||||
"description": "Updated onboarding network page description"
|
||||
},
|
||||
"wizardNetworkFeature1": "Review team members, repeaters, rooms, channels, and pending adverts in one list.",
|
||||
"@wizardNetworkFeature1": {
|
||||
"description": "Updated onboarding network feature 1"
|
||||
},
|
||||
"wizardNetworkFeature2": "Use smart ping, room login, learned paths, and route reset tools when connectivity gets messy.",
|
||||
"@wizardNetworkFeature2": {
|
||||
"description": "Updated onboarding network feature 2"
|
||||
},
|
||||
"wizardNetworkFeature3": "Create channels and manage network destinations without leaving the app.",
|
||||
"@wizardNetworkFeature3": {
|
||||
"description": "Updated onboarding network feature 3"
|
||||
},
|
||||
"wizardMapOpsTitle": "Map, Trails, and Shared Geometry",
|
||||
"@wizardMapOpsTitle": {
|
||||
"description": "Updated onboarding map page title"
|
||||
},
|
||||
"wizardMapOpsDescription": "The app map is tied directly into messaging, tracking, and SAR overlays instead of being a separate viewer.",
|
||||
"@wizardMapOpsDescription": {
|
||||
"description": "Updated onboarding map page description"
|
||||
},
|
||||
"wizardMapOpsFeature1": "Track your own position, teammate locations, and movement trails on the map.",
|
||||
"@wizardMapOpsFeature1": {
|
||||
"description": "Updated onboarding map feature 1"
|
||||
},
|
||||
"wizardMapOpsFeature2": "Open drawings from messages, preview them inline, and remove them from the map when needed.",
|
||||
"@wizardMapOpsFeature2": {
|
||||
"description": "Updated onboarding map feature 2"
|
||||
},
|
||||
"wizardMapOpsFeature3": "Use repeater map views and shared overlays to understand network reach in the field.",
|
||||
"@wizardMapOpsFeature3": {
|
||||
"description": "Updated onboarding map feature 3"
|
||||
},
|
||||
"wizardToolsTitle": "Tools Beyond Messaging",
|
||||
"@wizardToolsTitle": {
|
||||
"description": "Updated onboarding tools page title"
|
||||
},
|
||||
"wizardToolsDescription": "There is more here than the four main tabs. The app also includes configuration, diagnostics, and optional sensor workflows.",
|
||||
"@wizardToolsDescription": {
|
||||
"description": "Updated onboarding tools page description"
|
||||
},
|
||||
"wizardToolsFeature1": "Open device config to change radio settings, telemetry, TX power, and companion details.",
|
||||
"@wizardToolsFeature1": {
|
||||
"description": "Updated onboarding tools feature 1"
|
||||
},
|
||||
"wizardToolsFeature2": "Enable the Sensors tab when you want watched sensor dashboards and quick refresh actions.",
|
||||
"@wizardToolsFeature2": {
|
||||
"description": "Updated onboarding tools feature 2"
|
||||
},
|
||||
"wizardToolsFeature3": "Use packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.",
|
||||
"@wizardToolsFeature3": {
|
||||
"description": "Updated onboarding tools feature 3"
|
||||
},
|
||||
"contactInSensors": "In Sensors",
|
||||
"@contactInSensors": {
|
||||
"description": "Contact actions label when already in sensors"
|
||||
},
|
||||
"contactAddToSensors": "Add to Sensors",
|
||||
"@contactAddToSensors": {
|
||||
"description": "Contact actions label to add to sensors"
|
||||
},
|
||||
"contactSetPath": "Set path",
|
||||
"@contactSetPath": {
|
||||
"description": "Contact actions label to set route path"
|
||||
},
|
||||
"contactAddedToSensors": "{contactName} added to Sensors",
|
||||
"@contactAddedToSensors": {
|
||||
"description": "Snackbar after adding contact to sensors",
|
||||
"placeholders": {
|
||||
"contactName": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactFailedToClearRoute": "Failed to clear route: {error}",
|
||||
"@contactFailedToClearRoute": {
|
||||
"description": "Error when clearing a contact route fails",
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactRouteCleared": "Route cleared",
|
||||
"@contactRouteCleared": {
|
||||
"description": "Snackbar after route is cleared"
|
||||
},
|
||||
"contactRouteSet": "Route set: {route}",
|
||||
"@contactRouteSet": {
|
||||
"description": "Snackbar after route is set",
|
||||
"placeholders": {
|
||||
"route": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"contactFailedToSetRoute": "Failed to set route: {error}",
|
||||
"@contactFailedToSetRoute": {
|
||||
"description": "Error when setting a contact route fails",
|
||||
"placeholders": {
|
||||
"error": {
|
||||
"type": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rssi": "RSSI",
|
||||
"@rssi": {
|
||||
"description": "Received signal strength indicator"
|
||||
},
|
||||
"snr": "SNR",
|
||||
"@snr": {
|
||||
"description": "Signal-to-noise ratio"
|
||||
},
|
||||
"ackTimeout": "ACK timeout",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Payload",
|
||||
"hops": "Hops",
|
||||
"hashSize": "Hash size",
|
||||
"pathBytes": "Path bytes",
|
||||
"selectedPath": "Selected path",
|
||||
"estimatedTx": "Estimated tx",
|
||||
"senderToReceipt": "Sender to receipt",
|
||||
"receivedCopies": "Received copies",
|
||||
"retryCause": "Retry cause",
|
||||
"retryMode": "Retry mode",
|
||||
"retryResult": "Retry result",
|
||||
"lastRetry": "Last retry",
|
||||
"rxPackets": "RX packets",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Rate",
|
||||
"window": "Window",
|
||||
"posttxDelay": "Post-tx delay",
|
||||
"bandpass": "Band-pass",
|
||||
"bandpassFilterVoice": "Band-pass filter voice",
|
||||
"frequency": "Frequency",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Narrow)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "New Zealand",
|
||||
"newZealandNarrow": "New Zealand (Narrow)",
|
||||
"switzerland": "Switzerland",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Czech Republic (Narrow)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Long Range)",
|
||||
"euukDeprecated": "EU/UK (Deprecated)",
|
||||
"euukNarrow": "EU/UK (Narrow)",
|
||||
"usacanadaRecommended": "USA/Canada (Recommended)",
|
||||
"vietnamDeprecated": "Vietnam (Deprecated)",
|
||||
"vietnamNarrow": "Vietnam (Narrow)",
|
||||
"active": "Active",
|
||||
"addContact": "Add Contact",
|
||||
"all": "All",
|
||||
"autoResolve": "Auto resolve",
|
||||
"clearAllLabel": "Clear all",
|
||||
"clearRelays": "Clear relays",
|
||||
"clearFilters": "Clear filters",
|
||||
"clearRoute": "Clear Route",
|
||||
"clearMessages": "Clear Messages",
|
||||
"clearScale": "Clear scale",
|
||||
"clearDiscoveries": "Clear discoveries",
|
||||
"clearOnlineTraceDatabase": "Clear online trace database",
|
||||
"clearAllChannels": "Clear all channels",
|
||||
"clearAllContacts": "Clear all contacts",
|
||||
"clearChannels": "Clear channels",
|
||||
"clearContacts": "Clear contacts",
|
||||
"clearPathOnMaxRetry": "Clear path on max retry",
|
||||
"create": "Create",
|
||||
"custom": "Custom",
|
||||
"defaultValue": "Default",
|
||||
"duplicate": "Duplicate",
|
||||
"editName": "Edit name",
|
||||
"open": "Open",
|
||||
"paste": "Paste",
|
||||
"preview": "Preview",
|
||||
"remove": "Remove",
|
||||
"rename": "Rename",
|
||||
"resolveAll": "Resolve all",
|
||||
"send": "Send",
|
||||
"sendAnyway": "Send anyway",
|
||||
"share": "Share",
|
||||
"shareContact": "Share Contact",
|
||||
"trace": "Trace",
|
||||
"use": "Use",
|
||||
"useSelectedFrequency": "Use selected frequency",
|
||||
"discovery": "Discovery",
|
||||
"discoverRepeaters": "Discover repeaters",
|
||||
"discoverSensors": "Discover sensors",
|
||||
"repeaterDiscoverySent": "Repeater discovery sent",
|
||||
"sensorDiscoverySent": "Sensor discovery sent",
|
||||
"clearedPendingDiscoveries": "Cleared pending discoveries.",
|
||||
"autoDiscovery": "Auto discovery",
|
||||
"enableAutomaticAdding": "Enable automatic adding",
|
||||
"autoaddRepeaters": "Auto-add repeaters",
|
||||
"autoaddRoomServers": "Auto-add room servers",
|
||||
"autoaddSensors": "Auto-add sensors",
|
||||
"autoaddUsers": "Auto-add users",
|
||||
"overwriteOldestWhenFull": "Overwrite oldest when full",
|
||||
"storage": "Storage",
|
||||
"dangerZone": "Danger zone",
|
||||
"profiles": "Profiles",
|
||||
"favourites": "Favourites",
|
||||
"sensors": "Sensors",
|
||||
"others": "Others",
|
||||
"gpsModule": "GPS Module",
|
||||
"liveTraffic": "Live Traffic",
|
||||
"repeatersMap": "Repeaters Map",
|
||||
"spectrumScan": "Spectrum Scan",
|
||||
"blePacketLogs": "BLE Packet Logs",
|
||||
"onlineTraceDatabase": "Online trace database",
|
||||
"routePathByteSize": "Route path byte size",
|
||||
"messageNotifications": "Message notifications",
|
||||
"sarAlerts": "SAR alerts",
|
||||
"discoveryNotifications": "Discovery notifications",
|
||||
"updateNotifications": "Update notifications",
|
||||
"muteWhileAppIsOpen": "Mute while app is open",
|
||||
"disableContacts": "Disable Contacts",
|
||||
"enableSensorsTab": "Enable Sensors tab",
|
||||
"enableProfiles": "Enable Profiles",
|
||||
"autoRouteRotation": "Auto route rotation",
|
||||
"nearestRepeaterFallback": "Nearest repeater fallback",
|
||||
"deleteAllStoredMessageHistory": "Delete all stored message history",
|
||||
"messageFontSize": "Message font size",
|
||||
"rotateMapWithHeading": "Rotate map with heading",
|
||||
"showMapDebugInfo": "Show map debug info",
|
||||
"openMapInFullscreen": "Open map in fullscreen",
|
||||
"showSarMarkersLabel": "Show SAR markers",
|
||||
"displaySarMarkersOnTheMainMap": "Display SAR markers on the main map",
|
||||
"showAllContactTrailsLabel": "Show all contact trails",
|
||||
"hideRepeatersOnMap": "Hide repeaters on map",
|
||||
"setMapScale": "Set map scale",
|
||||
"customMapScaleSaved": "Custom map scale saved",
|
||||
"voiceBitrate": "Voice bitrate",
|
||||
"voiceCompressor": "Voice compressor",
|
||||
"balancesQuietAndLoudSpeechLevels": "Balances quiet and loud speech levels",
|
||||
"voiceLimiter": "Voice limiter",
|
||||
"preventsClippingPeaksBeforeEncoding": "Prevents clipping peaks before encoding",
|
||||
"micAutoGain": "Mic auto gain",
|
||||
"letsTheRecorderAdjustInputLevel": "Lets the recorder adjust input level",
|
||||
"echoCancellation": "Echo cancellation",
|
||||
"noiseSuppression": "Noise suppression",
|
||||
"trimSilenceInVoiceMessages": "Trim silence in voice messages",
|
||||
"compressor": "Compressor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Auto gain",
|
||||
"echoCancel": "Echo cancel",
|
||||
"noiseSuppress": "Noise suppress",
|
||||
"silenceTrim": "Silence trim",
|
||||
"maxImageSize": "Max image size",
|
||||
"imageCompression": "Image compression",
|
||||
"grayscale": "Grayscale",
|
||||
"ultraMode": "Ultra mode",
|
||||
"fastPrivateGpsUpdates": "Fast private GPS updates",
|
||||
"movementThreshold": "Movement threshold",
|
||||
"fastGpsMovementThreshold": "Fast GPS movement threshold",
|
||||
"fastGpsActiveuseInterval": "Fast GPS active-use interval",
|
||||
"activeuseUpdateInterval": "Active-use update interval",
|
||||
"repeatNearbyTraffic": "Repeat nearby traffic",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Relay through repeaters across the mesh",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Nearby only, without repeater flooding",
|
||||
"multihop": "Multi-hop",
|
||||
"createProfile": "Create Profile",
|
||||
"renameProfile": "Rename Profile",
|
||||
"newProfile": "New Profile",
|
||||
"manageProfiles": "Manage profiles",
|
||||
"enableProfilesToStartManagingThem": "Enable profiles to start managing them.",
|
||||
"openMessage": "Open message",
|
||||
"jumpToTheRelatedSarMessage": "Jump to the related SAR message",
|
||||
"removeSarMarker": "Remove SAR marker",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Please select a destination to send SAR marker",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR marker broadcast to public channel",
|
||||
"sarMarkerSentToRoom": "SAR marker sent to room",
|
||||
"loadFromGallery": "Load from gallery",
|
||||
"replaceImage": "Replace image",
|
||||
"selectFromGallery": "Select from gallery",
|
||||
"team": "Team",
|
||||
"found": "Found",
|
||||
"staging": "Staging",
|
||||
"object": "Object",
|
||||
"quiet": "Quiet",
|
||||
"moderate": "Moderate",
|
||||
"busy": "Busy",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spectrum scan returned no candidate frequencies",
|
||||
"searchMessages": "Search messages",
|
||||
"sendImageFromGallery": "Send image from gallery",
|
||||
"takePhoto": "Take photo",
|
||||
"dmOnly": "DM only",
|
||||
"allMessages": "All messages",
|
||||
"sendToPublicChannel": "Send to Public Channel?",
|
||||
"selectMarkerTypeAndDestination": "Select marker type and destination",
|
||||
"noDestinationsAvailableLabel": "No destinations available",
|
||||
"image": "Image",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimensions",
|
||||
"segments": "Segments",
|
||||
"transfers": "Transfers",
|
||||
"downloadedBy": "Downloaded by",
|
||||
"saveDiscoverySettings": "Save discovery settings",
|
||||
"savePublicInfo": "Save public info",
|
||||
"saveRadioSettings": "Save radio settings",
|
||||
"savePath": "Save Path",
|
||||
"wipeDeviceData": "Wipe device data",
|
||||
"wipeDevice": "Wipe device",
|
||||
"destructiveDeviceActions": "Destructive device actions.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Choose a preset or fine-tune custom radio settings.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Choose the name and location this device shares.",
|
||||
"availableSpaceOnThisDevice": "Available space on this device.",
|
||||
"used": "Used",
|
||||
"total": "Total",
|
||||
"renameValue": "Rename value",
|
||||
"customizeFields": "Customize fields",
|
||||
"livePreview": "Live preview",
|
||||
"refreshSchedule": "Refresh schedule",
|
||||
"noResponse": "No response",
|
||||
"refreshing": "Refreshing",
|
||||
"unavailable": "Unavailable",
|
||||
"pickARelayOrNodeToWatchInSensors": "Pick a relay or node to watch in Sensors.",
|
||||
"publicKeyLabel": "Public key",
|
||||
"alreadyInContacts": "Already in contacts",
|
||||
"connectToADeviceBeforeAddingContacts": "Connect to a device before adding contacts",
|
||||
"fromContacts": "From contacts",
|
||||
"onlineOnly": "Online only",
|
||||
"inBoth": "In both",
|
||||
"source": "Source",
|
||||
"manualRouteEdit": "Manual route edit",
|
||||
"observedMeshRoute": "Observed mesh route",
|
||||
"allMessagesCleared": "All messages cleared",
|
||||
"onlineTraceDatabaseCleared": "Online trace database cleared",
|
||||
"packetLogsCleared": "Packet logs cleared",
|
||||
"hexDataCopiedToClipboard": "Hex data copied to clipboard",
|
||||
"developerModeEnabled": "Developer mode enabled",
|
||||
"developerModeDisabled": "Developer mode disabled",
|
||||
"clipboardIsEmpty": "Clipboard is empty",
|
||||
"contactImported": "Contact imported",
|
||||
"contactLinkCopiedToClipboard": "Contact link copied to clipboard",
|
||||
"failedToExportContact": "Failed to export contact",
|
||||
"noLogsToExport": "No logs to export",
|
||||
"exportAsCsv": "Export as CSV",
|
||||
"exportAsText": "Export as Text",
|
||||
"receivedRfc3339": "Received (RFC3339)",
|
||||
"buildTime": "Build Time",
|
||||
"downloadUrlNotAvailable": "Download URL not available",
|
||||
"cannotOpenDownloadUrl": "Cannot open download URL",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Update check is only available on Android",
|
||||
"youAreRunningTheLatestVersion": "You are running the latest version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Update available but download URL not found",
|
||||
"startTictactoe": "Start Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe unavailable",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: opponent unknown",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: waiting for start",
|
||||
"acceptsShareLinks": "Accepts share links",
|
||||
"supportsRawHex": "Supports raw hex",
|
||||
"clipboardfriendly": "Clipboard-friendly",
|
||||
"captured": "Captured",
|
||||
"size": "Size",
|
||||
"noCustomChannelsToClear": "No custom channels to clear.",
|
||||
"noDeviceContactsToClear": "No device contacts to clear."
|
||||
}
|
||||
|
||||
@@ -1586,5 +1586,304 @@
|
||||
"rawDump": "Volcado sin procesar",
|
||||
"cannotRetryMissingRecipient": "No se puede reintentar: falta información del destinatario",
|
||||
"voiceUnavailable": "Voz no disponible en este momento",
|
||||
"requestingVoice": "Solicitando voz"
|
||||
"requestingVoice": "Solicitando voz",
|
||||
"device": "dispositivo",
|
||||
"change": "Cambiar",
|
||||
"wizardOverviewDescription": "Esta aplicación reúne mensajería MeshCore, actualizaciones SAR de campo, mapas y herramientas del dispositivo en un solo lugar.",
|
||||
"wizardOverviewFeature1": "Envía mensajes directos, publicaciones en salas y mensajes de canal desde la pestaña principal Mensajes.",
|
||||
"wizardOverviewFeature2": "Comparte marcadores SAR, dibujos del mapa, clips de voz e imágenes a través de la malla.",
|
||||
"wizardOverviewFeature3": "Conéctate por BLE o TCP y luego gestiona la radio complementaria desde la aplicación.",
|
||||
"wizardMessagingTitle": "Mensajería e informes de campo",
|
||||
"wizardMessagingDescription": "Aquí los mensajes son más que texto plano. La aplicación ya admite varias cargas operativas y flujos de transferencia.",
|
||||
"wizardMessagingFeature1": "Envía mensajes directos, publicaciones en salas y tráfico de canal desde un solo editor.",
|
||||
"wizardMessagingFeature2": "Crea actualizaciones SAR y plantillas SAR reutilizables para informes de campo habituales.",
|
||||
"wizardMessagingFeature3": "Transfiere sesiones de voz e imágenes con progreso y estimaciones de tiempo de aire en la interfaz.",
|
||||
"wizardConnectDeviceTitle": "Conectar dispositivo",
|
||||
"wizardConnectDeviceDescription": "Conecta tu radio MeshCore, elige un nombre y aplica un ajuste predefinido antes de continuar.",
|
||||
"wizardSetupBadge": "Configuración",
|
||||
"wizardOverviewBadge": "Resumen",
|
||||
"wizardConnectedToDevice": "Conectado a {deviceName}",
|
||||
"wizardNoDeviceConnected": "Aún no hay ningún dispositivo conectado",
|
||||
"wizardSkipForNow": "Omitir por ahora",
|
||||
"wizardDeviceNameLabel": "Nombre del dispositivo",
|
||||
"wizardDeviceNameHelp": "Este nombre se anuncia a otros usuarios de MeshCore.",
|
||||
"wizardConfigRegionLabel": "Región de configuración",
|
||||
"wizardConfigRegionHelp": "Usa la lista oficial completa de ajustes predefinidos de MeshCore. El valor predeterminado es EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Asegúrate de que el ajuste seleccionado coincida con la normativa local de radio.",
|
||||
"wizardPresetNote2": "La lista coincide con la fuente oficial de ajustes predefinidos de la herramienta de configuración de MeshCore.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) sigue seleccionado por defecto durante la incorporación.",
|
||||
"wizardSaving": "Guardando...",
|
||||
"wizardSaveAndContinue": "Guardar y continuar",
|
||||
"wizardEnterDeviceName": "Introduce un nombre de dispositivo antes de continuar.",
|
||||
"wizardDeviceSetupSaved": "Se guardó {deviceName} con {presetName}.",
|
||||
"wizardNetworkTitle": "Contactos, salas y repetidores",
|
||||
"wizardNetworkDescription": "La pestaña Contactos organiza la red que descubres y las rutas que aprendes con el tiempo.",
|
||||
"wizardNetworkFeature1": "Revisa miembros del equipo, repetidores, salas, canales y anuncios pendientes en una sola lista.",
|
||||
"wizardNetworkFeature2": "Usa ping inteligente, acceso a salas, rutas aprendidas y herramientas de reinicio de ruta cuando la conectividad se complique.",
|
||||
"wizardNetworkFeature3": "Crea canales y gestiona destinos de red sin salir de la aplicación.",
|
||||
"wizardMapOpsTitle": "Mapa, rastros y geometría compartida",
|
||||
"wizardMapOpsDescription": "El mapa de la aplicación está vinculado directamente a la mensajería, el seguimiento y las superposiciones SAR en lugar de ser un visor aparte.",
|
||||
"wizardMapOpsFeature1": "Sigue tu propia posición, las ubicaciones de tus compañeros y los rastros de movimiento en el mapa.",
|
||||
"wizardMapOpsFeature2": "Abre dibujos desde mensajes, prévisualos en línea y elimínalos del mapa cuando sea necesario.",
|
||||
"wizardMapOpsFeature3": "Usa vistas de mapa de repetidores y superposiciones compartidas para comprender el alcance de la red sobre el terreno.",
|
||||
"wizardToolsTitle": "Herramientas más allá de la mensajería",
|
||||
"wizardToolsDescription": "Aquí hay más que las cuatro pestañas principales. La aplicación también incluye configuración, diagnóstico y flujos opcionales de sensores.",
|
||||
"wizardToolsFeature1": "Abre la configuración del dispositivo para cambiar ajustes de radio, telemetría, potencia TX y detalles del equipo complementario.",
|
||||
"wizardToolsFeature2": "Activa la pestaña Sensores cuando quieras paneles de sensores vigilados y acciones de actualización rápida.",
|
||||
"wizardToolsFeature3": "Usa registros de paquetes, escaneo de espectro y diagnósticos de desarrollador al solucionar problemas de la malla.",
|
||||
"contactInSensors": "En Sensores",
|
||||
"contactAddToSensors": "Añadir a Sensores",
|
||||
"contactSetPath": "Definir ruta",
|
||||
"contactAddedToSensors": "{contactName} se añadió a Sensores",
|
||||
"contactFailedToClearRoute": "No se pudo borrar la ruta: {error}",
|
||||
"contactRouteCleared": "Ruta borrada",
|
||||
"contactRouteSet": "Ruta establecida: {route}",
|
||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tiempo de espera ACK",
|
||||
"opcode": "Código de operación",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamaño del hash",
|
||||
"pathBytes": "Bytes de ruta",
|
||||
"selectedPath": "Ruta seleccionada",
|
||||
"estimatedTx": "Transmisión estimada",
|
||||
"senderToReceipt": "Del remitente al recibo",
|
||||
"receivedCopies": "Copias recibidas",
|
||||
"retryCause": "Causa del reintento",
|
||||
"retryMode": "Modo de reintento",
|
||||
"retryResult": "Resultado del reintento",
|
||||
"lastRetry": "Último reintento",
|
||||
"rxPackets": "Paquetes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocidad",
|
||||
"window": "Ventana",
|
||||
"posttxDelay": "Retardo post-tx",
|
||||
"bandpass": "Paso de banda",
|
||||
"bandpassFilterVoice": "Filtro de paso de banda de voz",
|
||||
"frequency": "Frecuencia",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Estrecho)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nueva Zelanda",
|
||||
"newZealandNarrow": "Nueva Zelanda (Estrecho)",
|
||||
"switzerland": "Suiza",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estrecho)",
|
||||
"eu433mhzLongRange": "EU 433MHz (Largo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estrecho)",
|
||||
"usacanadaRecommended": "EE.UU./Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietnam (Obsoleto)",
|
||||
"vietnamNarrow": "Vietnam (Estrecho)",
|
||||
"active": "Activo",
|
||||
"addContact": "Añadir contacto",
|
||||
"all": "Todo",
|
||||
"autoResolve": "Resolver automáticamente",
|
||||
"clearAllLabel": "Borrar todo",
|
||||
"clearRelays": "Borrar repetidores",
|
||||
"clearFilters": "Borrar filtros",
|
||||
"clearRoute": "Borrar ruta",
|
||||
"clearMessages": "Borrar mensajes",
|
||||
"clearScale": "Borrar escala",
|
||||
"clearDiscoveries": "Borrar descubrimientos",
|
||||
"clearOnlineTraceDatabase": "Borrar base de datos de trazas",
|
||||
"clearAllChannels": "Borrar todos los canales",
|
||||
"clearAllContacts": "Borrar todos los contactos",
|
||||
"clearChannels": "Borrar canales",
|
||||
"clearContacts": "Borrar contactos",
|
||||
"clearPathOnMaxRetry": "Borrar ruta en máximo reintento",
|
||||
"create": "Crear",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Predeterminado",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nombre",
|
||||
"open": "Abrir",
|
||||
"paste": "Pegar",
|
||||
"preview": "Vista previa",
|
||||
"remove": "Eliminar",
|
||||
"rename": "Renombrar",
|
||||
"resolveAll": "Resolver todo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar de todos modos",
|
||||
"share": "Compartir",
|
||||
"shareContact": "Compartir contacto",
|
||||
"trace": "Traza",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frecuencia seleccionada",
|
||||
"discovery": "Descubrimiento",
|
||||
"discoverRepeaters": "Descubrir repetidores",
|
||||
"discoverSensors": "Descubrir sensores",
|
||||
"repeaterDiscoverySent": "Descubrimiento de repetidores enviado",
|
||||
"sensorDiscoverySent": "Descubrimiento de sensores enviado",
|
||||
"clearedPendingDiscoveries": "Descubrimientos pendientes borrados.",
|
||||
"autoDiscovery": "Descubrimiento automático",
|
||||
"enableAutomaticAdding": "Habilitar adición automática",
|
||||
"autoaddRepeaters": "Añadir repetidores automáticamente",
|
||||
"autoaddRoomServers": "Añadir servidores de sala automáticamente",
|
||||
"autoaddSensors": "Añadir sensores automáticamente",
|
||||
"autoaddUsers": "Añadir usuarios automáticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescribir más antiguos cuando esté lleno",
|
||||
"storage": "Almacenamiento",
|
||||
"dangerZone": "Zona de peligro",
|
||||
"profiles": "Perfiles",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Otros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfico en vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Escaneo de espectro",
|
||||
"blePacketLogs": "Registros de paquetes BLE",
|
||||
"onlineTraceDatabase": "Base de datos de trazas",
|
||||
"routePathByteSize": "Tamaño en bytes de la ruta",
|
||||
"messageNotifications": "Notificaciones de mensajes",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificaciones de descubrimiento",
|
||||
"updateNotifications": "Notificaciones de actualización",
|
||||
"muteWhileAppIsOpen": "Silenciar con la app abierta",
|
||||
"disableContacts": "Desactivar contactos",
|
||||
"enableSensorsTab": "Habilitar pestaña Sensores",
|
||||
"enableProfiles": "Habilitar perfiles",
|
||||
"autoRouteRotation": "Rotación automática de ruta",
|
||||
"nearestRepeaterFallback": "Repetidor más cercano como reserva",
|
||||
"deleteAllStoredMessageHistory": "Eliminar todo el historial de mensajes",
|
||||
"messageFontSize": "Tamaño de fuente de mensajes",
|
||||
"rotateMapWithHeading": "Rotar mapa con rumbo",
|
||||
"showMapDebugInfo": "Mostrar info de depuración del mapa",
|
||||
"openMapInFullscreen": "Abrir mapa en pantalla completa",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR en el mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos los rastros de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores en el mapa",
|
||||
"setMapScale": "Establecer escala del mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Tasa de bits de voz",
|
||||
"voiceCompressor": "Compresor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra niveles de habla suaves y altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene recorte de picos antes de codificar",
|
||||
"micAutoGain": "Ganancia automática del micrófono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite al grabador ajustar el nivel de entrada",
|
||||
"echoCancellation": "Cancelación de eco",
|
||||
"noiseSuppression": "Supresión de ruido",
|
||||
"trimSilenceInVoiceMessages": "Recortar silencio en mensajes de voz",
|
||||
"compressor": "Compresor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganancia automática",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruido",
|
||||
"silenceTrim": "Silencio",
|
||||
"maxImageSize": "Tamaño máximo de imagen",
|
||||
"imageCompression": "Compresión de imagen",
|
||||
"grayscale": "Escala de grises",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Actualizaciones GPS privadas rápidas",
|
||||
"movementThreshold": "Umbral de movimiento",
|
||||
"fastGpsMovementThreshold": "Umbral de movimiento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso activo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de actualización de uso activo",
|
||||
"repeatNearbyTraffic": "Repetir tráfico cercano",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir a través de repetidores en la red",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo cercanos, sin inundación de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Crear perfil",
|
||||
"renameProfile": "Renombrar perfil",
|
||||
"newProfile": "Nuevo perfil",
|
||||
"manageProfiles": "Gestionar perfiles",
|
||||
"enableProfilesToStartManagingThem": "Habilite los perfiles para comenzar a gestionarlos.",
|
||||
"openMessage": "Abrir mensaje",
|
||||
"jumpToTheRelatedSarMessage": "Ir al mensaje SAR relacionado",
|
||||
"removeSarMarker": "Eliminar marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleccione un destino para enviar el marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido al canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado a la sala",
|
||||
"loadFromGallery": "Cargar desde galería",
|
||||
"replaceImage": "Reemplazar imagen",
|
||||
"selectFromGallery": "Seleccionar de galería",
|
||||
"team": "Equipo",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunión",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "El escaneo de espectro no encontró frecuencias candidatas",
|
||||
"searchMessages": "Buscar mensajes",
|
||||
"sendImageFromGallery": "Enviar imagen desde galería",
|
||||
"takePhoto": "Tomar foto",
|
||||
"dmOnly": "Solo mensaje directo",
|
||||
"allMessages": "Todos los mensajes",
|
||||
"sendToPublicChannel": "¿Enviar al canal público?",
|
||||
"selectMarkerTypeAndDestination": "Seleccione tipo de marcador y destino",
|
||||
"noDestinationsAvailableLabel": "No hay destinos disponibles",
|
||||
"image": "Imagen",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensiones",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferencias",
|
||||
"downloadedBy": "Descargado por",
|
||||
"saveDiscoverySettings": "Guardar ajustes de descubrimiento",
|
||||
"savePublicInfo": "Guardar información pública",
|
||||
"saveRadioSettings": "Guardar ajustes de radio",
|
||||
"savePath": "Guardar ruta",
|
||||
"wipeDeviceData": "Borrar datos del dispositivo",
|
||||
"wipeDevice": "Borrar dispositivo",
|
||||
"destructiveDeviceActions": "Acciones destructivas del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Elija un preajuste o ajuste la configuración de radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Elija el nombre y la ubicación que comparte este dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Espacio disponible en este dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renombrar valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Vista previa en vivo",
|
||||
"refreshSchedule": "Programación de actualización",
|
||||
"noResponse": "Sin respuesta",
|
||||
"refreshing": "Actualizando",
|
||||
"unavailable": "No disponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Elija un relay o nodo para observar.",
|
||||
"publicKeyLabel": "Clave pública",
|
||||
"alreadyInContacts": "Ya en contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conéctese a un dispositivo antes de añadir contactos",
|
||||
"fromContacts": "De contactos",
|
||||
"onlineOnly": "Solo en línea",
|
||||
"inBoth": "En ambos",
|
||||
"source": "Fuente",
|
||||
"manualRouteEdit": "Edición manual de ruta",
|
||||
"observedMeshRoute": "Ruta mesh observada",
|
||||
"allMessagesCleared": "Todos los mensajes borrados",
|
||||
"onlineTraceDatabaseCleared": "Base de datos de trazas borrada",
|
||||
"packetLogsCleared": "Registros de paquetes borrados",
|
||||
"hexDataCopiedToClipboard": "Datos hex copiados al portapapeles",
|
||||
"developerModeEnabled": "Modo desarrollador activado",
|
||||
"developerModeDisabled": "Modo desarrollador desactivado",
|
||||
"clipboardIsEmpty": "El portapapeles está vacío",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Enlace de contacto copiado al portapapeles",
|
||||
"failedToExportContact": "Error al exportar el contacto",
|
||||
"noLogsToExport": "No hay registros para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recibido (RFC3339)",
|
||||
"buildTime": "Hora de compilación",
|
||||
"downloadUrlNotAvailable": "URL de descarga no disponible",
|
||||
"cannotOpenDownloadUrl": "No se puede abrir la URL de descarga",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La comprobación de actualizaciones solo está disponible en Android",
|
||||
"youAreRunningTheLatestVersion": "Está ejecutando la última versión",
|
||||
"updateAvailableButDownloadUrlNotFound": "Actualización disponible pero URL de descarga no encontrada",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe no disponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconocido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: esperando inicio",
|
||||
"acceptsShareLinks": "Acepta enlaces compartidos",
|
||||
"supportsRawHex": "Soporta hex sin procesar",
|
||||
"clipboardfriendly": "Compatible con portapapeles",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamaño",
|
||||
"noCustomChannelsToClear": "No hay canales personalizados que borrar.",
|
||||
"noDeviceContactsToClear": "No hay contactos del dispositivo que borrar."
|
||||
}
|
||||
|
||||
@@ -1589,5 +1589,304 @@
|
||||
"rawDump": "Vidage brut",
|
||||
"cannotRetryMissingRecipient": "Impossible de réessayer : informations du destinataire manquantes",
|
||||
"voiceUnavailable": "Voix indisponible pour le moment",
|
||||
"requestingVoice": "Demande de voix"
|
||||
"requestingVoice": "Demande de voix",
|
||||
"device": "appareil",
|
||||
"change": "Modifier",
|
||||
"wizardOverviewDescription": "Cette application réunit la messagerie MeshCore, les mises à jour SAR sur le terrain, la cartographie et les outils de l'appareil au même endroit.",
|
||||
"wizardOverviewFeature1": "Envoyez des messages directs, des publications de salon et des messages de canal depuis l'onglet principal Messages.",
|
||||
"wizardOverviewFeature2": "Partagez des marqueurs SAR, des dessins de carte, des extraits vocaux et des images sur le réseau maillé.",
|
||||
"wizardOverviewFeature3": "Connectez-vous en BLE ou en TCP, puis gérez la radio compagnon directement dans l'application.",
|
||||
"wizardMessagingTitle": "Messagerie et rapports de terrain",
|
||||
"wizardMessagingDescription": "Ici, les messages sont plus que du texte brut. L'application prend déjà en charge plusieurs charges opérationnelles et flux de transfert.",
|
||||
"wizardMessagingFeature1": "Envoyez des messages directs, des publications de salon et du trafic de canal depuis un seul compositeur.",
|
||||
"wizardMessagingFeature2": "Créez des mises à jour SAR et des modèles SAR réutilisables pour les rapports de terrain courants.",
|
||||
"wizardMessagingFeature3": "Transférez des sessions vocales et des images avec la progression et les estimations de temps d'antenne dans l'interface.",
|
||||
"wizardConnectDeviceTitle": "Connecter l'appareil",
|
||||
"wizardConnectDeviceDescription": "Connectez votre radio MeshCore, choisissez un nom et appliquez un préréglage radio avant de continuer.",
|
||||
"wizardSetupBadge": "Configuration",
|
||||
"wizardOverviewBadge": "Aperçu",
|
||||
"wizardConnectedToDevice": "Connecté à {deviceName}",
|
||||
"wizardNoDeviceConnected": "Aucun appareil connecté pour le moment",
|
||||
"wizardSkipForNow": "Ignorer pour l'instant",
|
||||
"wizardDeviceNameLabel": "Nom de l'appareil",
|
||||
"wizardDeviceNameHelp": "Ce nom est annoncé aux autres utilisateurs de MeshCore.",
|
||||
"wizardConfigRegionLabel": "Région de configuration",
|
||||
"wizardConfigRegionHelp": "Utilise la liste officielle complète des préréglages MeshCore. La valeur par défaut est EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Assurez-vous que le préréglage sélectionné correspond à votre réglementation radio locale.",
|
||||
"wizardPresetNote2": "La liste correspond au flux officiel des préréglages de l'outil de configuration MeshCore.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) reste sélectionné par défaut pendant l'onboarding.",
|
||||
"wizardSaving": "Enregistrement...",
|
||||
"wizardSaveAndContinue": "Enregistrer et continuer",
|
||||
"wizardEnterDeviceName": "Saisissez un nom d'appareil avant de continuer.",
|
||||
"wizardDeviceSetupSaved": "{deviceName} enregistré avec {presetName}.",
|
||||
"wizardNetworkTitle": "Contacts, salons et répéteurs",
|
||||
"wizardNetworkDescription": "L'onglet Contacts organise le réseau que vous découvrez et les routes apprises au fil du temps.",
|
||||
"wizardNetworkFeature1": "Consultez les membres de l'équipe, les répéteurs, les salons, les canaux et les annonces en attente dans une seule liste.",
|
||||
"wizardNetworkFeature2": "Utilisez le smart ping, la connexion aux salons, les chemins appris et les outils de réinitialisation des routes lorsque la connectivité devient chaotique.",
|
||||
"wizardNetworkFeature3": "Créez des canaux et gérez les destinations réseau sans quitter l'application.",
|
||||
"wizardMapOpsTitle": "Carte, traces et géométrie partagée",
|
||||
"wizardMapOpsDescription": "La carte de l'application est directement liée à la messagerie, au suivi et aux couches SAR au lieu d'être un simple visualiseur séparé.",
|
||||
"wizardMapOpsFeature1": "Suivez votre position, celles de vos coéquipiers et les traces de déplacement sur la carte.",
|
||||
"wizardMapOpsFeature2": "Ouvrez les dessins depuis les messages, prévisualisez-les en ligne et supprimez-les de la carte si nécessaire.",
|
||||
"wizardMapOpsFeature3": "Utilisez les vues de carte des répéteurs et les couches partagées pour comprendre la portée du réseau sur le terrain.",
|
||||
"wizardToolsTitle": "Outils au-delà de la messagerie",
|
||||
"wizardToolsDescription": "Il y a plus ici que les quatre onglets principaux. L'application inclut aussi la configuration, le diagnostic et des workflows de capteurs optionnels.",
|
||||
"wizardToolsFeature1": "Ouvrez la configuration de l'appareil pour modifier les réglages radio, la télémétrie, la puissance TX et les détails du compagnon.",
|
||||
"wizardToolsFeature2": "Activez l'onglet Capteurs lorsque vous voulez des tableaux de bord surveillés et des actions d'actualisation rapide.",
|
||||
"wizardToolsFeature3": "Utilisez les journaux de paquets, le balayage du spectre et les diagnostics développeur pour dépanner le réseau maillé.",
|
||||
"contactInSensors": "Dans Capteurs",
|
||||
"contactAddToSensors": "Ajouter aux capteurs",
|
||||
"contactSetPath": "Définir le chemin",
|
||||
"contactAddedToSensors": "{contactName} ajouté aux capteurs",
|
||||
"contactFailedToClearRoute": "Échec de l'effacement de la route : {error}",
|
||||
"contactRouteCleared": "Route effacée",
|
||||
"contactRouteSet": "Route définie : {route}",
|
||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Délai ACK expiré",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Charge utile",
|
||||
"hops": "Sauts",
|
||||
"hashSize": "Taille du hash",
|
||||
"pathBytes": "Octets du chemin",
|
||||
"selectedPath": "Chemin sélectionné",
|
||||
"estimatedTx": "Émission estimée",
|
||||
"senderToReceipt": "Émetteur au reçu",
|
||||
"receivedCopies": "Copies reçues",
|
||||
"retryCause": "Cause de la relance",
|
||||
"retryMode": "Mode de relance",
|
||||
"retryResult": "Résultat de la relance",
|
||||
"lastRetry": "Dernière relance",
|
||||
"rxPackets": "Paquets RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Débit",
|
||||
"window": "Fenêtre",
|
||||
"posttxDelay": "Délai post-émission",
|
||||
"bandpass": "Passe-bande",
|
||||
"bandpassFilterVoice": "Filtre passe-bande voix",
|
||||
"frequency": "Fréquence",
|
||||
"australia": "Australie",
|
||||
"australiaNarrow": "Australie (Étroit)",
|
||||
"australiaQld": "Australie: QLD",
|
||||
"australiaSaWa": "Australie: SA, WA",
|
||||
"newZealand": "Nouvelle-Zélande",
|
||||
"newZealandNarrow": "Nouvelle-Zélande (Étroit)",
|
||||
"switzerland": "Suisse",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "République tchèque (Étroit)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longue Portée)",
|
||||
"euukDeprecated": "UE/RU (Obsolète)",
|
||||
"euukNarrow": "UE/RU (Étroit)",
|
||||
"usacanadaRecommended": "USA/Canada (Recommandé)",
|
||||
"vietnamDeprecated": "Vietnam (Obsolète)",
|
||||
"vietnamNarrow": "Vietnam (Étroit)",
|
||||
"active": "Actif",
|
||||
"addContact": "Ajouter un contact",
|
||||
"all": "Tout",
|
||||
"autoResolve": "Résolution automatique",
|
||||
"clearAllLabel": "Tout effacer",
|
||||
"clearRelays": "Effacer les relais",
|
||||
"clearFilters": "Effacer les filtres",
|
||||
"clearRoute": "Effacer la route",
|
||||
"clearMessages": "Effacer les messages",
|
||||
"clearScale": "Effacer l'échelle",
|
||||
"clearDiscoveries": "Effacer les découvertes",
|
||||
"clearOnlineTraceDatabase": "Effacer la base de traces",
|
||||
"clearAllChannels": "Effacer tous les canaux",
|
||||
"clearAllContacts": "Effacer tous les contacts",
|
||||
"clearChannels": "Effacer les canaux",
|
||||
"clearContacts": "Effacer les contacts",
|
||||
"clearPathOnMaxRetry": "Effacer le chemin au max de relances",
|
||||
"create": "Créer",
|
||||
"custom": "Personnalisé",
|
||||
"defaultValue": "Par défaut",
|
||||
"duplicate": "Dupliquer",
|
||||
"editName": "Modifier le nom",
|
||||
"open": "Ouvrir",
|
||||
"paste": "Coller",
|
||||
"preview": "Aperçu",
|
||||
"remove": "Supprimer",
|
||||
"rename": "Renommer",
|
||||
"resolveAll": "Résoudre tout",
|
||||
"send": "Envoyer",
|
||||
"sendAnyway": "Envoyer quand même",
|
||||
"share": "Partager",
|
||||
"shareContact": "Partager le contact",
|
||||
"trace": "Trace",
|
||||
"use": "Utiliser",
|
||||
"useSelectedFrequency": "Utiliser la fréquence sélectionnée",
|
||||
"discovery": "Découverte",
|
||||
"discoverRepeaters": "Découvrir les répéteurs",
|
||||
"discoverSensors": "Découvrir les capteurs",
|
||||
"repeaterDiscoverySent": "Découverte de répéteurs envoyée",
|
||||
"sensorDiscoverySent": "Découverte de capteurs envoyée",
|
||||
"clearedPendingDiscoveries": "Découvertes en attente effacées.",
|
||||
"autoDiscovery": "Découverte automatique",
|
||||
"enableAutomaticAdding": "Activer l'ajout automatique",
|
||||
"autoaddRepeaters": "Ajout auto des répéteurs",
|
||||
"autoaddRoomServers": "Ajout auto des serveurs de salle",
|
||||
"autoaddSensors": "Ajout auto des capteurs",
|
||||
"autoaddUsers": "Ajout auto des utilisateurs",
|
||||
"overwriteOldestWhenFull": "Écraser les plus anciens si plein",
|
||||
"storage": "Stockage",
|
||||
"dangerZone": "Zone de danger",
|
||||
"profiles": "Profils",
|
||||
"favourites": "Favoris",
|
||||
"sensors": "Capteurs",
|
||||
"others": "Autres",
|
||||
"gpsModule": "Module GPS",
|
||||
"liveTraffic": "Trafic en direct",
|
||||
"repeatersMap": "Carte des répéteurs",
|
||||
"spectrumScan": "Scan spectral",
|
||||
"blePacketLogs": "Journaux de paquets BLE",
|
||||
"onlineTraceDatabase": "Base de données de traces",
|
||||
"routePathByteSize": "Taille en octets du chemin",
|
||||
"messageNotifications": "Notifications de messages",
|
||||
"sarAlerts": "Alertes SAR",
|
||||
"discoveryNotifications": "Notifications de découverte",
|
||||
"updateNotifications": "Notifications de mise à jour",
|
||||
"muteWhileAppIsOpen": "Muet quand l'appli est ouverte",
|
||||
"disableContacts": "Désactiver les contacts",
|
||||
"enableSensorsTab": "Activer l'onglet Capteurs",
|
||||
"enableProfiles": "Activer les profils",
|
||||
"autoRouteRotation": "Rotation automatique de route",
|
||||
"nearestRepeaterFallback": "Répéteur le plus proche en repli",
|
||||
"deleteAllStoredMessageHistory": "Supprimer tout l'historique",
|
||||
"messageFontSize": "Taille de police des messages",
|
||||
"rotateMapWithHeading": "Tourner la carte avec le cap",
|
||||
"showMapDebugInfo": "Afficher les infos de débogage",
|
||||
"openMapInFullscreen": "Ouvrir la carte en plein écran",
|
||||
"showSarMarkersLabel": "Afficher les marqueurs SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Afficher les marqueurs SAR sur la carte",
|
||||
"showAllContactTrailsLabel": "Afficher toutes les traces de contacts",
|
||||
"hideRepeatersOnMap": "Masquer les répéteurs sur la carte",
|
||||
"setMapScale": "Définir l'échelle de la carte",
|
||||
"customMapScaleSaved": "Échelle de carte personnalisée enregistrée",
|
||||
"voiceBitrate": "Débit vocal",
|
||||
"voiceCompressor": "Compresseur vocal",
|
||||
"balancesQuietAndLoudSpeechLevels": "Équilibre les niveaux faibles et forts",
|
||||
"voiceLimiter": "Limiteur vocal",
|
||||
"preventsClippingPeaksBeforeEncoding": "Empêche l'écrêtage avant l'encodage",
|
||||
"micAutoGain": "Gain auto du micro",
|
||||
"letsTheRecorderAdjustInputLevel": "Laisse l'enregistreur ajuster le niveau d'entrée",
|
||||
"echoCancellation": "Annulation d'écho",
|
||||
"noiseSuppression": "Suppression du bruit",
|
||||
"trimSilenceInVoiceMessages": "Couper le silence dans les messages vocaux",
|
||||
"compressor": "Compresseur",
|
||||
"limiter": "Limiteur",
|
||||
"autoGain": "Gain automatique",
|
||||
"echoCancel": "Écho",
|
||||
"noiseSuppress": "Bruit",
|
||||
"silenceTrim": "Silence",
|
||||
"maxImageSize": "Taille d'image maximale",
|
||||
"imageCompression": "Compression d'image",
|
||||
"grayscale": "Niveaux de gris",
|
||||
"ultraMode": "Mode ultra",
|
||||
"fastPrivateGpsUpdates": "Mises à jour GPS privées rapides",
|
||||
"movementThreshold": "Seuil de mouvement",
|
||||
"fastGpsMovementThreshold": "Seuil de mouvement GPS rapide",
|
||||
"fastGpsActiveuseInterval": "Intervalle d'utilisation active GPS rapide",
|
||||
"activeuseUpdateInterval": "Intervalle de mise à jour en utilisation active",
|
||||
"repeatNearbyTraffic": "Répéter le trafic proche",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Relayer via les répéteurs du réseau",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Proximité seulement, sans inondation",
|
||||
"multihop": "Multi-saut",
|
||||
"createProfile": "Créer un profil",
|
||||
"renameProfile": "Renommer le profil",
|
||||
"newProfile": "Nouveau profil",
|
||||
"manageProfiles": "Gérer les profils",
|
||||
"enableProfilesToStartManagingThem": "Activez les profils pour commencer à les gérer.",
|
||||
"openMessage": "Ouvrir le message",
|
||||
"jumpToTheRelatedSarMessage": "Aller au message SAR associé",
|
||||
"removeSarMarker": "Supprimer le marqueur SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Sélectionnez une destination pour envoyer le marqueur",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marqueur SAR diffusé sur le canal public",
|
||||
"sarMarkerSentToRoom": "Marqueur SAR envoyé à la salle",
|
||||
"loadFromGallery": "Charger depuis la galerie",
|
||||
"replaceImage": "Remplacer l'image",
|
||||
"selectFromGallery": "Sélectionner depuis la galerie",
|
||||
"team": "Équipe",
|
||||
"found": "Trouvé",
|
||||
"staging": "Zone de regroupement",
|
||||
"object": "Objet",
|
||||
"quiet": "Calme",
|
||||
"moderate": "Modéré",
|
||||
"busy": "Occupé",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Le scan spectral n'a trouvé aucune fréquence candidate",
|
||||
"searchMessages": "Rechercher des messages",
|
||||
"sendImageFromGallery": "Envoyer une image de la galerie",
|
||||
"takePhoto": "Prendre une photo",
|
||||
"dmOnly": "Message direct uniquement",
|
||||
"allMessages": "Tous les messages",
|
||||
"sendToPublicChannel": "Envoyer au canal public ?",
|
||||
"selectMarkerTypeAndDestination": "Sélectionnez le type de marqueur et la destination",
|
||||
"noDestinationsAvailableLabel": "Aucune destination disponible",
|
||||
"image": "Image",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimensions",
|
||||
"segments": "Segments",
|
||||
"transfers": "Transferts",
|
||||
"downloadedBy": "Téléchargé par",
|
||||
"saveDiscoverySettings": "Enregistrer les paramètres de découverte",
|
||||
"savePublicInfo": "Enregistrer les infos publiques",
|
||||
"saveRadioSettings": "Enregistrer les paramètres radio",
|
||||
"savePath": "Enregistrer le chemin",
|
||||
"wipeDeviceData": "Effacer les données de l'appareil",
|
||||
"wipeDevice": "Effacer l'appareil",
|
||||
"destructiveDeviceActions": "Actions destructives de l'appareil.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Choisissez un préréglage ou ajustez les paramètres radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Choisissez le nom et l'emplacement partagés par cet appareil.",
|
||||
"availableSpaceOnThisDevice": "Espace disponible sur cet appareil.",
|
||||
"used": "Utilisé",
|
||||
"total": "Total",
|
||||
"renameValue": "Renommer la valeur",
|
||||
"customizeFields": "Personnaliser les champs",
|
||||
"livePreview": "Aperçu en direct",
|
||||
"refreshSchedule": "Planification d'actualisation",
|
||||
"noResponse": "Pas de réponse",
|
||||
"refreshing": "Actualisation",
|
||||
"unavailable": "Indisponible",
|
||||
"pickARelayOrNodeToWatchInSensors": "Choisissez un relais ou nœud à surveiller.",
|
||||
"publicKeyLabel": "Clé publique",
|
||||
"alreadyInContacts": "Déjà dans les contacts",
|
||||
"connectToADeviceBeforeAddingContacts": "Connectez-vous à un appareil avant d'ajouter des contacts",
|
||||
"fromContacts": "Des contacts",
|
||||
"onlineOnly": "En ligne uniquement",
|
||||
"inBoth": "Dans les deux",
|
||||
"source": "Source",
|
||||
"manualRouteEdit": "Édition manuelle de route",
|
||||
"observedMeshRoute": "Route mesh observée",
|
||||
"allMessagesCleared": "Tous les messages effacés",
|
||||
"onlineTraceDatabaseCleared": "Base de données de traces effacée",
|
||||
"packetLogsCleared": "Journaux de paquets effacés",
|
||||
"hexDataCopiedToClipboard": "Données hex copiées dans le presse-papiers",
|
||||
"developerModeEnabled": "Mode développeur activé",
|
||||
"developerModeDisabled": "Mode développeur désactivé",
|
||||
"clipboardIsEmpty": "Le presse-papiers est vide",
|
||||
"contactImported": "Contact importé",
|
||||
"contactLinkCopiedToClipboard": "Lien du contact copié dans le presse-papiers",
|
||||
"failedToExportContact": "Échec de l'exportation du contact",
|
||||
"noLogsToExport": "Aucun journal à exporter",
|
||||
"exportAsCsv": "Exporter en CSV",
|
||||
"exportAsText": "Exporter en texte",
|
||||
"receivedRfc3339": "Reçu (RFC3339)",
|
||||
"buildTime": "Heure de build",
|
||||
"downloadUrlNotAvailable": "URL de téléchargement non disponible",
|
||||
"cannotOpenDownloadUrl": "Impossible d'ouvrir l'URL de téléchargement",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "La vérification des mises à jour n'est disponible que sur Android",
|
||||
"youAreRunningTheLatestVersion": "Vous utilisez la dernière version",
|
||||
"updateAvailableButDownloadUrlNotFound": "Mise à jour disponible mais URL de téléchargement introuvable",
|
||||
"startTictactoe": "Démarrer Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponible",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: adversaire inconnu",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: en attente du début",
|
||||
"acceptsShareLinks": "Accepte les liens partagés",
|
||||
"supportsRawHex": "Supporte le hex brut",
|
||||
"clipboardfriendly": "Compatible avec le presse-papiers",
|
||||
"captured": "Capturé",
|
||||
"size": "Taille",
|
||||
"noCustomChannelsToClear": "Aucun canal personnalisé à effacer.",
|
||||
"noDeviceContactsToClear": "Aucun contact de l'appareil à effacer."
|
||||
}
|
||||
|
||||
@@ -511,5 +511,304 @@
|
||||
"rawDump": "Sirovi ispis",
|
||||
"cannotRetryMissingRecipient": "Nije moguće ponoviti: nedostaju informacije o primatelju",
|
||||
"voiceUnavailable": "Glas trenutno nije dostupan",
|
||||
"requestingVoice": "Zahtjev za glasom"
|
||||
"requestingVoice": "Zahtjev za glasom",
|
||||
"device": "uređaj",
|
||||
"change": "Promijeni",
|
||||
"wizardOverviewDescription": "Ova aplikacija objedinjuje MeshCore poruke, SAR ažuriranja s terena, karte i alate za uređaj na jednom mjestu.",
|
||||
"wizardOverviewFeature1": "Šaljite izravne poruke, objave u sobama i poruke kanala iz glavne kartice Poruke.",
|
||||
"wizardOverviewFeature2": "Dijelite SAR oznake, crteže karte, glasovne isječke i slike preko mesh mreže.",
|
||||
"wizardOverviewFeature3": "Povežite se putem BLE-a ili TCP-a, a zatim upravljajte pratećim radijem izravno iz aplikacije.",
|
||||
"wizardMessagingTitle": "Poruke i terenska izvješća",
|
||||
"wizardMessagingDescription": "Poruke ovdje nisu samo običan tekst. Aplikacija već podržava više operativnih sadržaja i tijekova prijenosa.",
|
||||
"wizardMessagingFeature1": "Šaljite izravne poruke, objave u sobama i promet kanala iz jednog uređivača.",
|
||||
"wizardMessagingFeature2": "Stvarajte SAR ažuriranja i višekratne SAR predloške za uobičajena terenska izvješća.",
|
||||
"wizardMessagingFeature3": "Prenosite glasovne sesije i slike uz prikaz napretka i procjene vremena prijenosa u sučelju.",
|
||||
"wizardConnectDeviceTitle": "Poveži uređaj",
|
||||
"wizardConnectDeviceDescription": "Povežite svoj MeshCore radio, odaberite naziv i primijenite radijski predložak prije nastavka.",
|
||||
"wizardSetupBadge": "Postavljanje",
|
||||
"wizardOverviewBadge": "Pregled",
|
||||
"wizardConnectedToDevice": "Povezano s uređajem {deviceName}",
|
||||
"wizardNoDeviceConnected": "Još nema povezanog uređaja",
|
||||
"wizardSkipForNow": "Preskoči za sada",
|
||||
"wizardDeviceNameLabel": "Naziv uređaja",
|
||||
"wizardDeviceNameHelp": "Taj se naziv oglašava drugim korisnicima MeshCorea.",
|
||||
"wizardConfigRegionLabel": "Regija konfiguracije",
|
||||
"wizardConfigRegionHelp": "Koristi puni službeni popis MeshCore predložaka. Zadana je opcija EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Provjerite odgovara li odabrani predložak lokalnim radijskim propisima.",
|
||||
"wizardPresetNote2": "Popis odgovara službenom izvoru predložaka alata MeshCore config.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) ostaje zadano odabran tijekom uvodnog postavljanja.",
|
||||
"wizardSaving": "Spremanje...",
|
||||
"wizardSaveAndContinue": "Spremi i nastavi",
|
||||
"wizardEnterDeviceName": "Unesite naziv uređaja prije nastavka.",
|
||||
"wizardDeviceSetupSaved": "Spremljeno {deviceName} s postavkom {presetName}.",
|
||||
"wizardNetworkTitle": "Kontakti, sobe i repetitori",
|
||||
"wizardNetworkDescription": "Kartica Kontakti organizira mrežu koju otkrijete i rute koje s vremenom naučite.",
|
||||
"wizardNetworkFeature1": "Pregledajte članove tima, repetitore, sobe, kanale i oglase na čekanju na jednom popisu.",
|
||||
"wizardNetworkFeature2": "Koristite pametni ping, prijavu u sobe, naučene putanje i alate za resetiranje ruta kada povezanost postane neuredna.",
|
||||
"wizardNetworkFeature3": "Stvarajte kanale i upravljajte mrežnim odredištima bez napuštanja aplikacije.",
|
||||
"wizardMapOpsTitle": "Karta, tragovi i dijeljena geometrija",
|
||||
"wizardMapOpsDescription": "Karta u aplikaciji izravno je povezana s porukama, praćenjem i SAR slojevima umjesto da bude zaseban preglednik.",
|
||||
"wizardMapOpsFeature1": "Pratite vlastitu poziciju, lokacije suigrača i tragove kretanja na karti.",
|
||||
"wizardMapOpsFeature2": "Otvorite crteže iz poruka, pregledajte ih unutar sučelja i po potrebi ih uklonite s karte.",
|
||||
"wizardMapOpsFeature3": "Koristite karte repetitora i dijeljene slojeve kako biste razumjeli doseg mreže na terenu.",
|
||||
"wizardToolsTitle": "Alati izvan poruka",
|
||||
"wizardToolsDescription": "Ovdje postoji više od četiri glavne kartice. Aplikacija uključuje i konfiguraciju, dijagnostiku te opcionalne radne tokove senzora.",
|
||||
"wizardToolsFeature1": "Otvorite konfiguraciju uređaja kako biste promijenili radijske postavke, telemetriju, TX snagu i detalje pratećeg uređaja.",
|
||||
"wizardToolsFeature2": "Omogućite karticu Senzori kada želite nadzorne ploče praćenih senzora i brza osvježavanja.",
|
||||
"wizardToolsFeature3": "Koristite zapisnike paketa, skeniranje spektra i razvojnu dijagnostiku pri rješavanju problema s mesh mrežom.",
|
||||
"contactInSensors": "U Senzorima",
|
||||
"contactAddToSensors": "Dodaj u Senzore",
|
||||
"contactSetPath": "Postavi putanju",
|
||||
"contactAddedToSensors": "{contactName} dodan u Senzore",
|
||||
"contactFailedToClearRoute": "Brisanje rute nije uspjelo: {error}",
|
||||
"contactRouteCleared": "Ruta obrisana",
|
||||
"contactRouteSet": "Ruta postavljena: {route}",
|
||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK istek vremena",
|
||||
"opcode": "Opkod",
|
||||
"payload": "Sadržaj",
|
||||
"hops": "Skokovi",
|
||||
"hashSize": "Veličina hash-a",
|
||||
"pathBytes": "Bajtovi putanje",
|
||||
"selectedPath": "Odabrana putanja",
|
||||
"estimatedTx": "Procijenjeno slanje",
|
||||
"senderToReceipt": "Od pošiljatelja do primitka",
|
||||
"receivedCopies": "Primljene kopije",
|
||||
"retryCause": "Razlog ponovnog pokušaja",
|
||||
"retryMode": "Način ponovnog pokušaja",
|
||||
"retryResult": "Rezultat ponovnog pokušaja",
|
||||
"lastRetry": "Zadnji pokušaj",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Brzina",
|
||||
"window": "Prozor",
|
||||
"posttxDelay": "Kašnjenje nakon slanja",
|
||||
"bandpass": "Pojasni filtar",
|
||||
"bandpassFilterVoice": "Pojasni filtar glasa",
|
||||
"frequency": "Frekvencija",
|
||||
"australia": "Australija",
|
||||
"australiaNarrow": "Australija (usko)",
|
||||
"australiaQld": "Australija: QLD",
|
||||
"australiaSaWa": "Australija: SA, WA",
|
||||
"newZealand": "Novi Zeland",
|
||||
"newZealandNarrow": "Novi Zeland (usko)",
|
||||
"switzerland": "Švicarska",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "Češka (usko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (veliki domet)",
|
||||
"euukDeprecated": "EU/UK (zastarjelo)",
|
||||
"euukNarrow": "EU/UK (usko)",
|
||||
"usacanadaRecommended": "SAD/Kanada (preporučeno)",
|
||||
"vietnamDeprecated": "Vijetnam (zastarjelo)",
|
||||
"vietnamNarrow": "Vijetnam (usko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Sve",
|
||||
"autoResolve": "Automatski razriješi",
|
||||
"clearAllLabel": "Očisti sve",
|
||||
"clearRelays": "Očisti posrednike",
|
||||
"clearFilters": "Očisti filtre",
|
||||
"clearRoute": "Očisti putanju",
|
||||
"clearMessages": "Očisti poruke",
|
||||
"clearScale": "Očisti mjerilo",
|
||||
"clearDiscoveries": "Očisti otkrića",
|
||||
"clearOnlineTraceDatabase": "Očisti bazu mrežnih tragova",
|
||||
"clearAllChannels": "Očisti sve kanale",
|
||||
"clearAllContacts": "Očisti sve kontakte",
|
||||
"clearChannels": "Očisti kanale",
|
||||
"clearContacts": "Očisti kontakte",
|
||||
"clearPathOnMaxRetry": "Očisti putanju kod maks. pokušaja",
|
||||
"create": "Stvori",
|
||||
"custom": "Prilagođeno",
|
||||
"defaultValue": "Zadano",
|
||||
"duplicate": "Dupliciraj",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Otvori",
|
||||
"paste": "Zalijepi",
|
||||
"preview": "Pregled",
|
||||
"remove": "Ukloni",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razriješi sve",
|
||||
"send": "Pošalji",
|
||||
"sendAnyway": "Svejedno pošalji",
|
||||
"share": "Dijeli",
|
||||
"shareContact": "Dijeli kontakt",
|
||||
"trace": "Praćenje",
|
||||
"use": "Koristi",
|
||||
"useSelectedFrequency": "Koristi odabranu frekvenciju",
|
||||
"discovery": "Otkrivanje",
|
||||
"discoverRepeaters": "Otkrij posrednike",
|
||||
"discoverSensors": "Otkrij senzore",
|
||||
"repeaterDiscoverySent": "Otkrivanje posrednika poslano",
|
||||
"sensorDiscoverySent": "Otkrivanje senzora poslano",
|
||||
"clearedPendingDiscoveries": "Očišćena čekajuća otkrića.",
|
||||
"autoDiscovery": "Automatsko otkrivanje",
|
||||
"enableAutomaticAdding": "Omogući automatsko dodavanje",
|
||||
"autoaddRepeaters": "Automatski dodaj posrednike",
|
||||
"autoaddRoomServers": "Automatski dodaj poslužitelje soba",
|
||||
"autoaddSensors": "Automatski dodaj senzore",
|
||||
"autoaddUsers": "Automatski dodaj korisnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarije kad je puno",
|
||||
"storage": "Pohrana",
|
||||
"dangerZone": "Opasna zona",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Favoriti",
|
||||
"sensors": "Senzori",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet uživo",
|
||||
"repeatersMap": "Karta posrednika",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "BLE zapisi paketa",
|
||||
"onlineTraceDatabase": "Baza mrežnih tragova",
|
||||
"routePathByteSize": "Veličina putanje u bajtima",
|
||||
"messageNotifications": "Obavijesti o porukama",
|
||||
"sarAlerts": "SAR upozorenja",
|
||||
"discoveryNotifications": "Obavijesti o otkrivanju",
|
||||
"updateNotifications": "Obavijesti o ažuriranju",
|
||||
"muteWhileAppIsOpen": "Utišaj dok je aplikacija otvorena",
|
||||
"disableContacts": "Onemogući kontakte",
|
||||
"enableSensorsTab": "Omogući karticu Senzori",
|
||||
"enableProfiles": "Omogući profile",
|
||||
"autoRouteRotation": "Automatsko rotiranje putanje",
|
||||
"nearestRepeaterFallback": "Najbliži posrednik kao rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši svu pohranjenu povijest poruka",
|
||||
"messageFontSize": "Veličina fonta poruka",
|
||||
"rotateMapWithHeading": "Rotiraj kartu prema smjeru",
|
||||
"showMapDebugInfo": "Prikaži debug info karte",
|
||||
"openMapInFullscreen": "Otvori kartu na cijelom zaslonu",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnoj karti",
|
||||
"showAllContactTrailsLabel": "Prikaži sve tragove kontakata",
|
||||
"hideRepeatersOnMap": "Sakrij posrednike na karti",
|
||||
"setMapScale": "Postavi mjerilo karte",
|
||||
"customMapScaleSaved": "Prilagođeno mjerilo karte spremljeno",
|
||||
"voiceBitrate": "Bitna brzina glasa",
|
||||
"voiceCompressor": "Kompresor glasa",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe i glasne razine govora",
|
||||
"voiceLimiter": "Ograničivač glasa",
|
||||
"preventsClippingPeaksBeforeEncoding": "Sprječava rezanje vrhova prije kodiranja",
|
||||
"micAutoGain": "Automatsko pojačanje mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Dopušta snimaču prilagodbu razine ulaza",
|
||||
"echoCancellation": "Poništavanje odjeka",
|
||||
"noiseSuppression": "Potiskivanje šuma",
|
||||
"trimSilenceInVoiceMessages": "Izreži tišinu u glasovnim porukama",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Ograničivač",
|
||||
"autoGain": "Automatsko pojačanje",
|
||||
"echoCancel": "Odjek",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Maksimalna veličina slike",
|
||||
"imageCompression": "Kompresija slike",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Brza privatna GPS ažuriranja",
|
||||
"movementThreshold": "Prag kretanja",
|
||||
"fastGpsMovementThreshold": "Prag kretanja za brzi GPS",
|
||||
"fastGpsActiveuseInterval": "Interval aktivnog korištenja brzog GPS-a",
|
||||
"activeuseUpdateInterval": "Interval ažuriranja pri aktivnom korištenju",
|
||||
"repeatNearbyTraffic": "Ponovi obližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Proslijedi kroz posrednike kroz mrežu",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo obližnji, bez preplavljivanja posrednika",
|
||||
"multihop": "Višeskočno",
|
||||
"createProfile": "Stvori profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Novi profil",
|
||||
"manageProfiles": "Upravljaj profilima",
|
||||
"enableProfilesToStartManagingThem": "Omogućite profile da biste ih počeli upravljati.",
|
||||
"openMessage": "Otvori poruku",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezanu SAR poruku",
|
||||
"removeSarMarker": "Ukloni SAR oznaku",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Odaberite odredište za slanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka emitirana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana u sobu",
|
||||
"loadFromGallery": "Učitaj iz galerije",
|
||||
"replaceImage": "Zamijeni sliku",
|
||||
"selectFromGallery": "Odaberi iz galerije",
|
||||
"team": "Tim",
|
||||
"found": "Pronađeno",
|
||||
"staging": "Sabirno mjesto",
|
||||
"object": "Objekt",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Umjereno",
|
||||
"busy": "Zauzeto",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje nije pronašlo prikladne frekvencije",
|
||||
"searchMessages": "Pretraži poruke",
|
||||
"sendImageFromGallery": "Pošalji sliku iz galerije",
|
||||
"takePhoto": "Snimi fotografiju",
|
||||
"dmOnly": "Samo izravno",
|
||||
"allMessages": "Sve poruke",
|
||||
"sendToPublicChannel": "Pošalji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Odaberite vrstu oznake i odredište",
|
||||
"noDestinationsAvailableLabel": "Nema dostupnih odredišta",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prijenosi",
|
||||
"downloadedBy": "Preuzeo",
|
||||
"saveDiscoverySettings": "Spremi postavke otkrivanja",
|
||||
"savePublicInfo": "Spremi javne podatke",
|
||||
"saveRadioSettings": "Spremi radijske postavke",
|
||||
"savePath": "Spremi putanju",
|
||||
"wipeDeviceData": "Obriši podatke uređaja",
|
||||
"wipeDevice": "Obriši uređaj",
|
||||
"destructiveDeviceActions": "Destruktivne radnje na uređaju.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Odaberite predefiniranu postavku ili fino podesite radijske postavke.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Odaberite ime i lokaciju koju ovaj uređaj dijeli.",
|
||||
"availableSpaceOnThisDevice": "Raspoloživi prostor na ovom uređaju.",
|
||||
"used": "Korišteno",
|
||||
"total": "Ukupno",
|
||||
"renameValue": "Preimenuj vrijednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Pregled uživo",
|
||||
"refreshSchedule": "Raspored osvježavanja",
|
||||
"noResponse": "Nema odgovora",
|
||||
"refreshing": "Osvježavam",
|
||||
"unavailable": "Nedostupno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Odaberite posrednika ili čvor za praćenje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Već u kontaktima",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se s uređajem prije dodavanja kontakata",
|
||||
"fromContacts": "Iz kontakata",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "U oba",
|
||||
"source": "Izvor",
|
||||
"manualRouteEdit": "Ručno uređivanje putanje",
|
||||
"observedMeshRoute": "Opažena mrežna putanja",
|
||||
"allMessagesCleared": "Sve poruke očišćene",
|
||||
"onlineTraceDatabaseCleared": "Baza mrežnih tragova očišćena",
|
||||
"packetLogsCleared": "Zapisi paketa očišćeni",
|
||||
"hexDataCopiedToClipboard": "Hex podaci kopirani u međuspremnik",
|
||||
"developerModeEnabled": "Razvojni način omogućen",
|
||||
"developerModeDisabled": "Razvojni način onemogućen",
|
||||
"clipboardIsEmpty": "Međuspremnik je prazan",
|
||||
"contactImported": "Kontakt uvezen",
|
||||
"contactLinkCopiedToClipboard": "Poveznica na kontakt kopirana u međuspremnik",
|
||||
"failedToExportContact": "Izvoz kontakta nije uspio",
|
||||
"noLogsToExport": "Nema zapisa za izvoz",
|
||||
"exportAsCsv": "Izvezi kao CSV",
|
||||
"exportAsText": "Izvezi kao tekst",
|
||||
"receivedRfc3339": "Primljeno (RFC3339)",
|
||||
"buildTime": "Vrijeme gradnje",
|
||||
"downloadUrlNotAvailable": "URL za preuzimanje nije dostupan",
|
||||
"cannotOpenDownloadUrl": "Ne mogu otvoriti URL za preuzimanje",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Provjera ažuriranja dostupna je samo na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Koristite najnoviju verziju",
|
||||
"updateAvailableButDownloadUrlNotFound": "Ažuriranje dostupno, ali URL za preuzimanje nije pronađen",
|
||||
"startTictactoe": "Pokreni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe nije dostupan",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: protivnik nepoznat",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čekanje na početak",
|
||||
"acceptsShareLinks": "Prihvaća dijeljene veze",
|
||||
"supportsRawHex": "Podržava sirovi hex",
|
||||
"clipboardfriendly": "Pogodno za međuspremnik",
|
||||
"captured": "Zabilježeno",
|
||||
"size": "Veličina",
|
||||
"noCustomChannelsToClear": "Nema prilagođenih kanala za brisanje.",
|
||||
"noDeviceContactsToClear": "Nema kontakata uređaja za brisanje."
|
||||
}
|
||||
|
||||
@@ -1589,5 +1589,304 @@
|
||||
"rawDump": "Dump grezzo",
|
||||
"cannotRetryMissingRecipient": "Impossibile riprovare: informazioni destinatario mancanti",
|
||||
"voiceUnavailable": "Voce non disponibile al momento",
|
||||
"requestingVoice": "Richiesta voce"
|
||||
"requestingVoice": "Richiesta voce",
|
||||
"device": "dispositivo",
|
||||
"change": "Modifica",
|
||||
"wizardOverviewDescription": "Questa app riunisce messaggistica MeshCore, aggiornamenti SAR sul campo, mappe e strumenti del dispositivo in un unico posto.",
|
||||
"wizardOverviewFeature1": "Invia messaggi diretti, post nelle stanze e messaggi di canale dalla scheda principale Messaggi.",
|
||||
"wizardOverviewFeature2": "Condividi marcatori SAR, disegni sulla mappa, clip vocali e immagini sulla mesh.",
|
||||
"wizardOverviewFeature3": "Connettiti tramite BLE o TCP e poi gestisci la radio companion direttamente dall'app.",
|
||||
"wizardMessagingTitle": "Messaggistica e rapporti sul campo",
|
||||
"wizardMessagingDescription": "Qui i messaggi sono più del semplice testo. L'app supporta già diversi payload operativi e flussi di trasferimento.",
|
||||
"wizardMessagingFeature1": "Invia messaggi diretti, post nelle stanze e traffico di canale da un unico compositore.",
|
||||
"wizardMessagingFeature2": "Crea aggiornamenti SAR e modelli SAR riutilizzabili per i rapporti sul campo più comuni.",
|
||||
"wizardMessagingFeature3": "Trasferisci sessioni vocali e immagini, con avanzamento e stime del tempo d'aria nell'interfaccia.",
|
||||
"wizardConnectDeviceTitle": "Connetti dispositivo",
|
||||
"wizardConnectDeviceDescription": "Connetti la tua radio MeshCore, scegli un nome e applica un preset radio prima di continuare.",
|
||||
"wizardSetupBadge": "Configurazione",
|
||||
"wizardOverviewBadge": "Panoramica",
|
||||
"wizardConnectedToDevice": "Connesso a {deviceName}",
|
||||
"wizardNoDeviceConnected": "Nessun dispositivo ancora connesso",
|
||||
"wizardSkipForNow": "Salta per ora",
|
||||
"wizardDeviceNameLabel": "Nome del dispositivo",
|
||||
"wizardDeviceNameHelp": "Questo nome viene annunciato agli altri utenti MeshCore.",
|
||||
"wizardConfigRegionLabel": "Regione di configurazione",
|
||||
"wizardConfigRegionHelp": "Usa l'elenco ufficiale completo dei preset MeshCore. Il valore predefinito è EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Assicurati che il preset selezionato corrisponda alle normative radio locali.",
|
||||
"wizardPresetNote2": "L'elenco corrisponde al feed ufficiale dei preset dello strumento di configurazione MeshCore.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) resta selezionato per impostazione predefinita durante l'onboarding.",
|
||||
"wizardSaving": "Salvataggio...",
|
||||
"wizardSaveAndContinue": "Salva e continua",
|
||||
"wizardEnterDeviceName": "Inserisci un nome dispositivo prima di continuare.",
|
||||
"wizardDeviceSetupSaved": "{deviceName} salvato con {presetName}.",
|
||||
"wizardNetworkTitle": "Contatti, stanze e ripetitori",
|
||||
"wizardNetworkDescription": "La scheda Contatti organizza la rete che scopri e i percorsi che apprendi nel tempo.",
|
||||
"wizardNetworkFeature1": "Controlla membri del team, ripetitori, stanze, canali e annunci in sospeso in un unico elenco.",
|
||||
"wizardNetworkFeature2": "Usa smart ping, accesso alle stanze, percorsi appresi e strumenti di reset delle route quando la connettività diventa complicata.",
|
||||
"wizardNetworkFeature3": "Crea canali e gestisci le destinazioni di rete senza uscire dall'app.",
|
||||
"wizardMapOpsTitle": "Mappa, tracce e geometria condivisa",
|
||||
"wizardMapOpsDescription": "La mappa dell'app è collegata direttamente a messaggistica, tracciamento e overlay SAR invece di essere un visualizzatore separato.",
|
||||
"wizardMapOpsFeature1": "Monitora la tua posizione, le posizioni dei compagni e le tracce di movimento sulla mappa.",
|
||||
"wizardMapOpsFeature2": "Apri i disegni dai messaggi, visualizzali in anteprima in linea e rimuovili dalla mappa quando serve.",
|
||||
"wizardMapOpsFeature3": "Usa le viste mappa dei ripetitori e gli overlay condivisi per capire la copertura della rete sul campo.",
|
||||
"wizardToolsTitle": "Strumenti oltre la messaggistica",
|
||||
"wizardToolsDescription": "Qui c'è più delle quattro schede principali. L'app include anche configurazione, diagnostica e flussi sensori opzionali.",
|
||||
"wizardToolsFeature1": "Apri la configurazione del dispositivo per cambiare impostazioni radio, telemetria, potenza TX e dettagli del companion.",
|
||||
"wizardToolsFeature2": "Abilita la scheda Sensori quando vuoi dashboard monitorate e azioni di aggiornamento rapido.",
|
||||
"wizardToolsFeature3": "Usa log dei pacchetti, scansione dello spettro e diagnostica sviluppatore per risolvere problemi della mesh.",
|
||||
"contactInSensors": "Nei Sensori",
|
||||
"contactAddToSensors": "Aggiungi ai Sensori",
|
||||
"contactSetPath": "Imposta percorso",
|
||||
"contactAddedToSensors": "{contactName} aggiunto a Sensori",
|
||||
"contactFailedToClearRoute": "Impossibile cancellare il percorso: {error}",
|
||||
"contactRouteCleared": "Percorso cancellato",
|
||||
"contactRouteSet": "Percorso impostato: {route}",
|
||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Timeout ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Payload",
|
||||
"hops": "Hop",
|
||||
"hashSize": "Dimensione hash",
|
||||
"pathBytes": "Byte percorso",
|
||||
"selectedPath": "Percorso selezionato",
|
||||
"estimatedTx": "Trasmissione stimata",
|
||||
"senderToReceipt": "Mittente a ricevuta",
|
||||
"receivedCopies": "Copie ricevute",
|
||||
"retryCause": "Causa del ritentativo",
|
||||
"retryMode": "Modalità ritentativo",
|
||||
"retryResult": "Risultato ritentativo",
|
||||
"lastRetry": "Ultimo ritentativo",
|
||||
"rxPackets": "Pacchetti RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Velocità",
|
||||
"window": "Finestra",
|
||||
"posttxDelay": "Ritardo post-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda voce",
|
||||
"frequency": "Frequenza",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (Stretto)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nuova Zelanda",
|
||||
"newZealandNarrow": "Nuova Zelanda (Stretto)",
|
||||
"switzerland": "Svizzera",
|
||||
"portugal433": "Portogallo 433",
|
||||
"portugal868": "Portogallo 868",
|
||||
"czechRepublicNarrow": "Repubblica Ceca (Stretto)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Lungo Raggio)",
|
||||
"euukDeprecated": "UE/UK (Deprecato)",
|
||||
"euukNarrow": "UE/UK (Stretto)",
|
||||
"usacanadaRecommended": "USA/Canada (Consigliato)",
|
||||
"vietnamDeprecated": "Vietnam (Deprecato)",
|
||||
"vietnamNarrow": "Vietnam (Stretto)",
|
||||
"active": "Attivo",
|
||||
"addContact": "Aggiungi contatto",
|
||||
"all": "Tutto",
|
||||
"autoResolve": "Risolvi automaticamente",
|
||||
"clearAllLabel": "Cancella tutto",
|
||||
"clearRelays": "Cancella relay",
|
||||
"clearFilters": "Cancella filtri",
|
||||
"clearRoute": "Cancella percorso",
|
||||
"clearMessages": "Cancella messaggi",
|
||||
"clearScale": "Cancella scala",
|
||||
"clearDiscoveries": "Cancella scoperte",
|
||||
"clearOnlineTraceDatabase": "Cancella database tracce",
|
||||
"clearAllChannels": "Cancella tutti i canali",
|
||||
"clearAllContacts": "Cancella tutti i contatti",
|
||||
"clearChannels": "Cancella canali",
|
||||
"clearContacts": "Cancella contatti",
|
||||
"clearPathOnMaxRetry": "Cancella percorso al max ritentativo",
|
||||
"create": "Crea",
|
||||
"custom": "Personalizzato",
|
||||
"defaultValue": "Predefinito",
|
||||
"duplicate": "Duplica",
|
||||
"editName": "Modifica nome",
|
||||
"open": "Apri",
|
||||
"paste": "Incolla",
|
||||
"preview": "Anteprima",
|
||||
"remove": "Rimuovi",
|
||||
"rename": "Rinomina",
|
||||
"resolveAll": "Risolvi tutto",
|
||||
"send": "Invia",
|
||||
"sendAnyway": "Invia comunque",
|
||||
"share": "Condividi",
|
||||
"shareContact": "Condividi contatto",
|
||||
"trace": "Traccia",
|
||||
"use": "Usa",
|
||||
"useSelectedFrequency": "Usa frequenza selezionata",
|
||||
"discovery": "Scoperta",
|
||||
"discoverRepeaters": "Scopri ripetitori",
|
||||
"discoverSensors": "Scopri sensori",
|
||||
"repeaterDiscoverySent": "Scoperta ripetitori inviata",
|
||||
"sensorDiscoverySent": "Scoperta sensori inviata",
|
||||
"clearedPendingDiscoveries": "Scoperte in attesa cancellate.",
|
||||
"autoDiscovery": "Scoperta automatica",
|
||||
"enableAutomaticAdding": "Abilita aggiunta automatica",
|
||||
"autoaddRepeaters": "Aggiungi ripetitori automaticamente",
|
||||
"autoaddRoomServers": "Aggiungi server stanze automaticamente",
|
||||
"autoaddSensors": "Aggiungi sensori automaticamente",
|
||||
"autoaddUsers": "Aggiungi utenti automaticamente",
|
||||
"overwriteOldestWhenFull": "Sovrascrivi i più vecchi quando pieno",
|
||||
"storage": "Archiviazione",
|
||||
"dangerZone": "Zona pericolosa",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Preferiti",
|
||||
"sensors": "Sensori",
|
||||
"others": "Altri",
|
||||
"gpsModule": "Modulo GPS",
|
||||
"liveTraffic": "Traffico in tempo reale",
|
||||
"repeatersMap": "Mappa ripetitori",
|
||||
"spectrumScan": "Scansione spettro",
|
||||
"blePacketLogs": "Log pacchetti BLE",
|
||||
"onlineTraceDatabase": "Database tracce",
|
||||
"routePathByteSize": "Dimensione percorso in byte",
|
||||
"messageNotifications": "Notifiche messaggi",
|
||||
"sarAlerts": "Avvisi SAR",
|
||||
"discoveryNotifications": "Notifiche scoperte",
|
||||
"updateNotifications": "Notifiche aggiornamenti",
|
||||
"muteWhileAppIsOpen": "Silenzia con l'app aperta",
|
||||
"disableContacts": "Disabilita contatti",
|
||||
"enableSensorsTab": "Abilita scheda Sensori",
|
||||
"enableProfiles": "Abilita profili",
|
||||
"autoRouteRotation": "Rotazione automatica percorso",
|
||||
"nearestRepeaterFallback": "Ripetitore più vicino come fallback",
|
||||
"deleteAllStoredMessageHistory": "Elimina tutta la cronologia messaggi",
|
||||
"messageFontSize": "Dimensione carattere messaggi",
|
||||
"rotateMapWithHeading": "Ruota mappa con direzione",
|
||||
"showMapDebugInfo": "Mostra info debug mappa",
|
||||
"openMapInFullscreen": "Apri mappa a schermo intero",
|
||||
"showSarMarkersLabel": "Mostra marcatori SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostra marcatori SAR sulla mappa principale",
|
||||
"showAllContactTrailsLabel": "Mostra tutti i percorsi contatti",
|
||||
"hideRepeatersOnMap": "Nascondi ripetitori sulla mappa",
|
||||
"setMapScale": "Imposta scala mappa",
|
||||
"customMapScaleSaved": "Scala mappa personalizzata salvata",
|
||||
"voiceBitrate": "Bitrate voce",
|
||||
"voiceCompressor": "Compressore voce",
|
||||
"balancesQuietAndLoudSpeechLevels": "Bilancia i livelli di voce bassi e alti",
|
||||
"voiceLimiter": "Limitatore voce",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previene il clipping prima della codifica",
|
||||
"micAutoGain": "Guadagno automatico microfono",
|
||||
"letsTheRecorderAdjustInputLevel": "Permette al registratore di regolare il livello",
|
||||
"echoCancellation": "Cancellazione eco",
|
||||
"noiseSuppression": "Soppressione rumore",
|
||||
"trimSilenceInVoiceMessages": "Taglia silenzio nei messaggi vocali",
|
||||
"compressor": "Compressore",
|
||||
"limiter": "Limitatore",
|
||||
"autoGain": "Guadagno automatico",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Rumore",
|
||||
"silenceTrim": "Silenzio",
|
||||
"maxImageSize": "Dimensione massima immagine",
|
||||
"imageCompression": "Compressione immagine",
|
||||
"grayscale": "Scala di grigi",
|
||||
"ultraMode": "Modalità ultra",
|
||||
"fastPrivateGpsUpdates": "Aggiornamenti GPS privati veloci",
|
||||
"movementThreshold": "Soglia di movimento",
|
||||
"fastGpsMovementThreshold": "Soglia movimento GPS veloce",
|
||||
"fastGpsActiveuseInterval": "Intervallo uso attivo GPS veloce",
|
||||
"activeuseUpdateInterval": "Intervallo aggiornamento uso attivo",
|
||||
"repeatNearbyTraffic": "Ripeti traffico vicino",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Inoltra tramite ripetitori nella rete",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Solo vicini, senza flood dei ripetitori",
|
||||
"multihop": "Multi-hop",
|
||||
"createProfile": "Crea profilo",
|
||||
"renameProfile": "Rinomina profilo",
|
||||
"newProfile": "Nuovo profilo",
|
||||
"manageProfiles": "Gestisci profili",
|
||||
"enableProfilesToStartManagingThem": "Abilita i profili per iniziare a gestirli.",
|
||||
"openMessage": "Apri messaggio",
|
||||
"jumpToTheRelatedSarMessage": "Vai al messaggio SAR correlato",
|
||||
"removeSarMarker": "Rimuovi marcatore SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Seleziona una destinazione per inviare il marcatore SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcatore SAR trasmesso al canale pubblico",
|
||||
"sarMarkerSentToRoom": "Marcatore SAR inviato alla stanza",
|
||||
"loadFromGallery": "Carica dalla galleria",
|
||||
"replaceImage": "Sostituisci immagine",
|
||||
"selectFromGallery": "Seleziona dalla galleria",
|
||||
"team": "Squadra",
|
||||
"found": "Trovato",
|
||||
"staging": "Area di raccolta",
|
||||
"object": "Oggetto",
|
||||
"quiet": "Silenzioso",
|
||||
"moderate": "Moderato",
|
||||
"busy": "Occupato",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "La scansione spettro non ha trovato frequenze candidate",
|
||||
"searchMessages": "Cerca messaggi",
|
||||
"sendImageFromGallery": "Invia immagine dalla galleria",
|
||||
"takePhoto": "Scatta foto",
|
||||
"dmOnly": "Solo messaggio diretto",
|
||||
"allMessages": "Tutti i messaggi",
|
||||
"sendToPublicChannel": "Inviare al canale pubblico?",
|
||||
"selectMarkerTypeAndDestination": "Seleziona tipo marcatore e destinazione",
|
||||
"noDestinationsAvailableLabel": "Nessuna destinazione disponibile",
|
||||
"image": "Immagine",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensioni",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Trasferimenti",
|
||||
"downloadedBy": "Scaricato da",
|
||||
"saveDiscoverySettings": "Salva impostazioni scoperta",
|
||||
"savePublicInfo": "Salva info pubbliche",
|
||||
"saveRadioSettings": "Salva impostazioni radio",
|
||||
"savePath": "Salva percorso",
|
||||
"wipeDeviceData": "Cancella dati dispositivo",
|
||||
"wipeDevice": "Cancella dispositivo",
|
||||
"destructiveDeviceActions": "Azioni distruttive del dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Scegli un preset o personalizza le impostazioni radio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Scegli il nome e la posizione condivisi da questo dispositivo.",
|
||||
"availableSpaceOnThisDevice": "Spazio disponibile su questo dispositivo.",
|
||||
"used": "Usato",
|
||||
"total": "Totale",
|
||||
"renameValue": "Rinomina valore",
|
||||
"customizeFields": "Personalizza campi",
|
||||
"livePreview": "Anteprima dal vivo",
|
||||
"refreshSchedule": "Programma aggiornamento",
|
||||
"noResponse": "Nessuna risposta",
|
||||
"refreshing": "Aggiornamento",
|
||||
"unavailable": "Non disponibile",
|
||||
"pickARelayOrNodeToWatchInSensors": "Scegli un relay o nodo da monitorare.",
|
||||
"publicKeyLabel": "Chiave pubblica",
|
||||
"alreadyInContacts": "Già nei contatti",
|
||||
"connectToADeviceBeforeAddingContacts": "Connettiti a un dispositivo prima di aggiungere contatti",
|
||||
"fromContacts": "Dai contatti",
|
||||
"onlineOnly": "Solo online",
|
||||
"inBoth": "In entrambi",
|
||||
"source": "Origine",
|
||||
"manualRouteEdit": "Modifica manuale percorso",
|
||||
"observedMeshRoute": "Percorso mesh osservato",
|
||||
"allMessagesCleared": "Tutti i messaggi cancellati",
|
||||
"onlineTraceDatabaseCleared": "Database tracce cancellata",
|
||||
"packetLogsCleared": "Log pacchetti cancellati",
|
||||
"hexDataCopiedToClipboard": "Dati hex copiati negli appunti",
|
||||
"developerModeEnabled": "Modalità sviluppatore attivata",
|
||||
"developerModeDisabled": "Modalità sviluppatore disattivata",
|
||||
"clipboardIsEmpty": "Gli appunti sono vuoti",
|
||||
"contactImported": "Contatto importato",
|
||||
"contactLinkCopiedToClipboard": "Link contatto copiato negli appunti",
|
||||
"failedToExportContact": "Esportazione contatto fallita",
|
||||
"noLogsToExport": "Nessun log da esportare",
|
||||
"exportAsCsv": "Esporta come CSV",
|
||||
"exportAsText": "Esporta come testo",
|
||||
"receivedRfc3339": "Ricevuto (RFC3339)",
|
||||
"buildTime": "Ora build",
|
||||
"downloadUrlNotAvailable": "URL di download non disponibile",
|
||||
"cannotOpenDownloadUrl": "Impossibile aprire URL di download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Il controllo aggiornamenti è disponibile solo su Android",
|
||||
"youAreRunningTheLatestVersion": "Stai utilizzando l'ultima versione",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aggiornamento disponibile ma URL di download non trovato",
|
||||
"startTictactoe": "Avvia Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe non disponibile",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: avversario sconosciuto",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: in attesa dell'inizio",
|
||||
"acceptsShareLinks": "Accetta link condivisi",
|
||||
"supportsRawHex": "Supporta hex grezzo",
|
||||
"clipboardfriendly": "Compatibile con appunti",
|
||||
"captured": "Catturato",
|
||||
"size": "Dimensione",
|
||||
"noCustomChannelsToClear": "Nessun canale personalizzato da cancellare.",
|
||||
"noDeviceContactsToClear": "Nessun contatto dispositivo da cancellare."
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1734,4 +1734,968 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Sprachnachricht wird angefordert';
|
||||
|
||||
@override
|
||||
String get device => 'Gerät';
|
||||
|
||||
@override
|
||||
String get change => 'Ändern';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Diese App vereint MeshCore-Nachrichten, SAR-Feldupdates, Karten und Gerätetools an einem Ort.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Sende Direkt-, Raum- und Kanalnachrichten aus dem Haupttab \"Nachrichten\".';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Teile SAR-Markierungen, Kartenzeichnungen, Sprachclips und Bilder über das Mesh.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Verbinde dich per BLE oder TCP und verwalte dann das Begleitfunkgerät direkt in der App.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Nachrichten und Feldberichte';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Nachrichten sind hier mehr als nur Klartext. Die App unterstützt bereits mehrere operative Datentypen und Übertragungsabläufe.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Sende Direktnachrichten, Raumbeiträge und Kanalverkehr aus einem einzigen Editor.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Erstelle SAR-Updates und wiederverwendbare SAR-Vorlagen für häufige Feldberichte.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Übertrage Sprachsitzungen und Bilder mit Fortschritts- und Airtime-Anzeigen in der Oberfläche.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Gerät verbinden';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Verbinde dein MeshCore-Funkgerät, wähle einen Namen und wende ein Funkprofil an, bevor du fortfährst.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Einrichtung';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Überblick';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Verbunden mit $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Noch kein Gerät verbunden';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Vorerst überspringen';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Gerätename';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Dieser Name wird an andere MeshCore-Nutzer ausgesendet.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Konfigurationsregion';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Verwendet die vollständige offizielle Liste der MeshCore-Voreinstellungen. Standard ist EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Stelle sicher, dass die ausgewählte Voreinstellung zu deinen lokalen Funkvorschriften passt.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Die Liste entspricht dem offiziellen Preset-Feed des MeshCore-Konfigurationstools.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) bleibt standardmäßig für das Onboarding ausgewählt.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Wird gespeichert...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Speichern und fortfahren';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Gib vor dem Fortfahren einen Gerätenamen ein.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '$deviceName mit $presetName gespeichert.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Kontakte, Räume und Repeater';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Der Kontakte-Tab organisiert das Netzwerk, das du entdeckst, und die Routen, die im Laufe der Zeit gelernt werden.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Prüfe Teammitglieder, Repeater, Räume, Kanäle und ausstehende Adverts in einer Liste.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Nutze Smart Ping, Raum-Login, gelernte Pfade und Pfad-Reset-Tools, wenn die Verbindung unübersichtlich wird.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Erstelle Kanäle und verwalte Netzwerkziele, ohne die App zu verlassen.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Karte, Spuren und geteilte Geometrie';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Die Karte der App ist direkt mit Nachrichten, Tracking und SAR-Overlays verbunden, statt nur ein separater Viewer zu sein.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Verfolge deine eigene Position, Teamstandorte und Bewegungsspuren auf der Karte.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Öffne Zeichnungen aus Nachrichten, prüfe sie inline und entferne sie bei Bedarf von der Karte.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Nutze Repeater-Kartenansichten und geteilte Overlays, um die Netzabdeckung im Feld zu verstehen.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Werkzeuge jenseits von Nachrichten';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Es gibt hier mehr als nur die vier Haupttabs. Die App enthält außerdem Konfiguration, Diagnose und optionale Sensor-Workflows.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Öffne die Gerätekonfiguration, um Funkparameter, Telemetrie, Sendeleistung und Begleitdetails zu ändern.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Aktiviere den Sensoren-Tab, wenn du überwachte Sensor-Dashboards und schnelle Aktualisierungen möchtest.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Nutze Paketprotokolle, Spektrumsuche und Entwicklerdiagnosen zur Fehlersuche im Mesh.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'In Sensoren';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Zu Sensoren hinzufügen';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Pfad festlegen';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName zu Sensoren hinzugefügt';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Pfad konnte nicht gelöscht werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Pfad gelöscht';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Pfad gesetzt: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Pfad konnte nicht gesetzt werden: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK-Zeitüberschreitung';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Nutzlast';
|
||||
|
||||
@override
|
||||
String get hops => 'Hops';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash-Größe';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Pfad-Bytes';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Ausgewählter Pfad';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Geschätzte Sendung';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Sender bis Empfang';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Empfangene Kopien';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Wiederholungsursache';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Wiederholungsmodus';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Wiederholungsergebnis';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Letzter Versuch';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-Pakete';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Rate';
|
||||
|
||||
@override
|
||||
String get window => 'Fenster';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Sendeverzögerung';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Bandpass';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Bandpassfilter Sprache';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequenz';
|
||||
|
||||
@override
|
||||
String get australia => 'Australien';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australien (Schmal)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australien: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australien: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Neuseeland';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Neuseeland (Schmal)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Schweiz';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Tschechien (Schmal)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Langstrecke)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (Veraltet)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (Schmal)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Kanada (Empfohlen)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Veraltet)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Schmal)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktiv';
|
||||
|
||||
@override
|
||||
String get addContact => 'Kontakt hinzufügen';
|
||||
|
||||
@override
|
||||
String get all => 'Alle';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatisch auflösen';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Alle löschen';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Relais löschen';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Filter löschen';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Route löschen';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Nachrichten löschen';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Maßstab löschen';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Entdeckungen löschen';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Online-Trace-Datenbank löschen';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Alle Kanäle löschen';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Alle Kontakte löschen';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Kanäle löschen';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Kontakte löschen';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Pfad bei max. Wiederholung löschen';
|
||||
|
||||
@override
|
||||
String get create => 'Erstellen';
|
||||
|
||||
@override
|
||||
String get custom => 'Benutzerdefiniert';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Standard';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplizieren';
|
||||
|
||||
@override
|
||||
String get editName => 'Name bearbeiten';
|
||||
|
||||
@override
|
||||
String get open => 'Öffnen';
|
||||
|
||||
@override
|
||||
String get paste => 'Einfügen';
|
||||
|
||||
@override
|
||||
String get preview => 'Vorschau';
|
||||
|
||||
@override
|
||||
String get remove => 'Entfernen';
|
||||
|
||||
@override
|
||||
String get rename => 'Umbenennen';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Alle auflösen';
|
||||
|
||||
@override
|
||||
String get send => 'Senden';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Trotzdem senden';
|
||||
|
||||
@override
|
||||
String get share => 'Teilen';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Kontakt teilen';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Verwenden';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Ausgewählte Frequenz verwenden';
|
||||
|
||||
@override
|
||||
String get discovery => 'Erkennung';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Repeater entdecken';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Sensoren entdecken';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Repeater-Erkennung gesendet';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensor-Erkennung gesendet';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Ausstehende Entdeckungen gelöscht.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatische Erkennung';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Automatisches Hinzufügen aktivieren';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Repeater automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Raumserver automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Sensoren automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Benutzer automatisch hinzufügen';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Älteste bei vollem Speicher überschreiben';
|
||||
|
||||
@override
|
||||
String get storage => 'Speicher';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Gefahrenbereich';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profile';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriten';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensoren';
|
||||
|
||||
@override
|
||||
String get others => 'Andere';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS-Modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Live-Verkehr';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Repeater-Karte';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektrum-Scan';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE-Paketprotokolle';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Online-Trace-Datenbank';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Pfad-Bytegröße';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Nachrichtenbenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-Alarme';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Erkennungsbenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Updatebenachrichtigungen';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Stumm bei geöffneter App';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Kontakte deaktivieren';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Sensoren-Tab aktivieren';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Profile aktivieren';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatische Routenrotation';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Nächster Repeater als Fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Gesamten Nachrichtenverlauf löschen';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Nachrichtenschriftgröße';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Karte mit Richtung drehen';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Karten-Debug-Info anzeigen';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Karte im Vollbild öffnen';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'SAR-Markierungen anzeigen';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'SAR-Markierungen auf der Hauptkarte anzeigen';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Alle Kontaktpfade anzeigen';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Repeater auf Karte ausblenden';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Kartenmaßstab einstellen';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved =>
|
||||
'Benutzerdefinierter Kartenmaßstab gespeichert';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Sprach-Bitrate';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Sprachkompressor';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Gleicht leise und laute Sprache aus';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Sprach-Limiter';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Verhindert Clipping-Spitzen vor der Kodierung';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mikrofon-Automatik';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Lässt den Rekorder den Eingangspegel anpassen';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Echounterdrückung';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Rauschunterdrückung';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Stille in Sprachnachrichten trimmen';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatische Verstärkung';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Rauschen';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Stille';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maximale Bildgröße';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Bildkomprimierung';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Graustufen';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra-Modus';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Schnelle private GPS-Updates';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Bewegungsschwelle';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Schneller GPS-Bewegungsschwellenwert';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Schnelles GPS aktive Nutzung Intervall';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Aktive Nutzung Updateintervall';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Nahverkehr wiederholen';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Über Repeater im Mesh weiterleiten';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Nur in der Nähe, ohne Repeater-Flooding';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-Hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Profil erstellen';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Profil umbenennen';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Neues Profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Profile verwalten';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Aktivieren Sie Profile, um sie zu verwalten.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Nachricht öffnen';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Zur zugehörigen SAR-Nachricht springen';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'SAR-Markierung entfernen';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Bitte wählen Sie ein Ziel zum Senden der SAR-Markierung';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-Markierung an öffentlichen Kanal gesendet';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-Markierung an Raum gesendet';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Aus Galerie laden';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Bild ersetzen';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Aus Galerie auswählen';
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get found => 'Gefunden';
|
||||
|
||||
@override
|
||||
String get staging => 'Sammelpunkt';
|
||||
|
||||
@override
|
||||
String get object => 'Objekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Ruhig';
|
||||
|
||||
@override
|
||||
String get moderate => 'Mäßig';
|
||||
|
||||
@override
|
||||
String get busy => 'Beschäftigt';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektrum-Scan hat keine Kandidatenfrequenzen gefunden';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Nachrichten suchen';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Bild aus Galerie senden';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Foto aufnehmen';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Nur Direktnachricht';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Alle Nachrichten';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'An öffentlichen Kanal senden?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Markierungstyp und Ziel auswählen';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Keine Ziele verfügbar';
|
||||
|
||||
@override
|
||||
String get image => 'Bild';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Abmessungen';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmente';
|
||||
|
||||
@override
|
||||
String get transfers => 'Übertragungen';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Heruntergeladen von';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Erkennungseinstellungen speichern';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Öffentliche Info speichern';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Funkeinstellungen speichern';
|
||||
|
||||
@override
|
||||
String get savePath => 'Pfad speichern';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Gerätedaten löschen';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Gerät löschen';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktive Geräteaktionen.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Wählen Sie eine Voreinstellung oder passen Sie die Funkeinstellungen an.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Wählen Sie den Namen und Standort, den dieses Gerät teilt.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Verfügbarer Speicherplatz auf diesem Gerät.';
|
||||
|
||||
@override
|
||||
String get used => 'Belegt';
|
||||
|
||||
@override
|
||||
String get total => 'Gesamt';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Wert umbenennen';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Felder anpassen';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Live-Vorschau';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Aktualisierungsplan';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Keine Antwort';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Wird aktualisiert';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Wählen Sie einen Relay oder Knoten zur Beobachtung.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Öffentlicher Schlüssel';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Bereits in Kontakten';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Verbinden Sie sich mit einem Gerät, bevor Sie Kontakte hinzufügen';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Aus Kontakten';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Nur online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In beiden';
|
||||
|
||||
@override
|
||||
String get source => 'Quelle';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manuelle Routenbearbeitung';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Beobachtete Mesh-Route';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Alle Nachrichten gelöscht';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Online-Trace-Datenbank gelöscht';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Paketprotokolle gelöscht';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Hex-Daten in die Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Entwicklermodus aktiviert';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Entwicklermodus deaktiviert';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Zwischenablage ist leer';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt importiert';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Kontaktlink in Zwischenablage kopiert';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Kontaktexport fehlgeschlagen';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Keine Protokolle zum Exportieren';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Als CSV exportieren';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Als Text exportieren';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Empfangen (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Build-Zeit';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Download-URL nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Download-URL kann nicht geöffnet werden';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Update-Prüfung nur auf Android verfügbar';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Sie verwenden die neueste Version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Update verfügbar, aber Download-URL nicht gefunden';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Tic-Tac-Toe starten';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe nicht verfügbar';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: Gegner unbekannt';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: Warten auf Start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Akzeptiert freigegebene Links';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Unterstützt rohes Hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Zwischenablage-freundlich';
|
||||
|
||||
@override
|
||||
String get captured => 'Erfasst';
|
||||
|
||||
@override
|
||||
String get size => 'Größe';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Keine benutzerdefinierten Kanäle zum Löschen.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Keine Gerätekontakte zum Löschen.';
|
||||
}
|
||||
|
||||
@@ -1741,4 +1741,963 @@ class AppLocalizationsEl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Αίτηση φωνής';
|
||||
|
||||
@override
|
||||
String get device => 'συσκευή';
|
||||
|
||||
@override
|
||||
String get change => 'Αλλαγή';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Αυτή η εφαρμογή συνδυάζει μηνύματα MeshCore, ενημερώσεις πεδίου SAR, χαρτογράφηση και εργαλεία συσκευής σε ένα σημείο.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Στείλτε άμεσα μηνύματα, δημοσιεύσεις δωματίου και μηνύματα καναλιού από την κύρια καρτέλα Μηνύματα.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Μοιραστείτε δείκτες SAR, σχεδιάσεις χάρτη, ηχητικά αποσπάσματα και εικόνες μέσω του mesh.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Συνδεθείτε μέσω BLE ή TCP και στη συνέχεια διαχειριστείτε το συνοδευτικό ραδιόφωνο μέσα από την εφαρμογή.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Μηνύματα και αναφορές πεδίου';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Τα μηνύματα εδώ είναι κάτι περισσότερο από απλό κείμενο. Η εφαρμογή υποστηρίζει ήδη πολλαπλά επιχειρησιακά φορτία και ροές μεταφοράς.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Στείλτε άμεσα μηνύματα, δημοσιεύσεις δωματίου και κίνηση καναλιού από έναν μόνο συντάκτη.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Δημιουργήστε ενημερώσεις SAR και επαναχρησιμοποιήσιμα πρότυπα SAR για συνηθισμένες αναφορές πεδίου.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Μεταφέρετε φωνητικές συνεδρίες και εικόνες, με ένδειξη προόδου και εκτιμήσεις χρόνου εκπομπής στο περιβάλλον.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Σύνδεση συσκευής';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Συνδέστε το ραδιόφωνο MeshCore, επιλέξτε όνομα και εφαρμόστε μια προρύθμιση ραδιοφώνου πριν συνεχίσετε.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Ρύθμιση';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Επισκόπηση';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Συνδέθηκε με το $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Δεν έχει συνδεθεί ακόμη συσκευή';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Παράλειψη προς το παρόν';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Όνομα συσκευής';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Αυτό το όνομα ανακοινώνεται σε άλλους χρήστες του MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Περιοχή ρύθμισης';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Χρησιμοποιεί την πλήρη επίσημη λίστα προρυθμίσεων MeshCore. Η προεπιλογή είναι EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Βεβαιωθείτε ότι η επιλεγμένη προρύθμιση ταιριάζει με τους τοπικούς ραδιοφωνικούς κανονισμούς.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Η λίστα αντιστοιχεί στην επίσημη ροή προρυθμίσεων του εργαλείου ρύθμισης MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'Το EU/UK (Narrow) παραμένει επιλεγμένο από προεπιλογή κατά την αρχική ρύθμιση.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Αποθήκευση...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Αποθήκευση και συνέχεια';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Εισαγάγετε όνομα συσκευής πριν συνεχίσετε.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Αποθηκεύτηκε το $deviceName με $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Επαφές, δωμάτια και αναμεταδότες';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Η καρτέλα Επαφές οργανώνει το δίκτυο που ανακαλύπτετε και τις διαδρομές που μαθαίνετε με τον χρόνο.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Ελέγξτε μέλη ομάδας, αναμεταδότες, δωμάτια, κανάλια και εκκρεμείς ανακοινώσεις σε μία λίστα.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Χρησιμοποιήστε smart ping, σύνδεση σε δωμάτια, μαθημένες διαδρομές και εργαλεία επαναφοράς διαδρομών όταν η συνδεσιμότητα γίνεται χαοτική.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Δημιουργήστε κανάλια και διαχειριστείτε προορισμούς δικτύου χωρίς να φύγετε από την εφαρμογή.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Χάρτης, ίχνη και κοινόχρηστη γεωμετρία';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Ο χάρτης της εφαρμογής συνδέεται άμεσα με τα μηνύματα, την παρακολούθηση και τα επικαλύμματα SAR αντί να είναι ξεχωριστός προβολέας.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Παρακολουθήστε τη θέση σας, τις τοποθεσίες της ομάδας και τα ίχνη κίνησης στον χάρτη.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Ανοίξτε σχεδιάσεις από μηνύματα, προεπισκοπήστε τες επιτόπου και αφαιρέστε τες από τον χάρτη όταν χρειάζεται.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Χρησιμοποιήστε προβολές χάρτη αναμεταδοτών και κοινόχρηστα επικαλύμματα για να κατανοήσετε την εμβέλεια του δικτύου στο πεδίο.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Εργαλεία πέρα από τα μηνύματα';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Υπάρχουν περισσότερα εδώ από τις τέσσερις κύριες καρτέλες. Η εφαρμογή περιλαμβάνει επίσης ρυθμίσεις, διαγνωστικά και προαιρετικές ροές αισθητήρων.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Ανοίξτε τη ρύθμιση συσκευής για να αλλάξετε παραμέτρους ραδιοφώνου, τηλεμετρία, ισχύ TX και στοιχεία συνοδευτικής συσκευής.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Ενεργοποιήστε την καρτέλα Αισθητήρες όταν θέλετε πίνακες παρακολούθησης και γρήγορες ενέργειες ανανέωσης.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Χρησιμοποιήστε καταγραφές πακέτων, σάρωση φάσματος και διαγνωστικά προγραμματιστή κατά την αντιμετώπιση προβλημάτων του mesh.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Στους αισθητήρες';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Προσθήκη στους αισθητήρες';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Ορισμός διαδρομής';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return 'Το $contactName προστέθηκε στους Αισθητήρες';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Αποτυχία εκκαθάρισης διαδρομής: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Η διαδρομή εκκαθαρίστηκε';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Ορίστηκε διαδρομή: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Αποτυχία ορισμού διαδρομής: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Λήξη χρόνου ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Ωφέλιμο φορτίο';
|
||||
|
||||
@override
|
||||
String get hops => 'Αναπηδήσεις';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Μέγεθος hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes διαδρομής';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Επιλεγμένη διαδρομή';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Εκτιμώμενη εκπομπή';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Αποστολέας σε απόδειξη';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Ληφθέντα αντίγραφα';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Αιτία επανάληψης';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Λειτουργία επανάληψης';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Αποτέλεσμα επανάληψης';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Τελευταία επανάληψη';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Πακέτα RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Ρυθμός';
|
||||
|
||||
@override
|
||||
String get window => 'Παράθυρο';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Καθυστέρηση μετά-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Ζωνοπερατό';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Ζωνοπερατό φίλτρο φωνής';
|
||||
|
||||
@override
|
||||
String get frequency => 'Συχνότητα';
|
||||
|
||||
@override
|
||||
String get australia => 'Αυστραλία';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Αυστραλία (Στενό)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Αυστραλία: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Αυστραλία: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Νέα Ζηλανδία';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Νέα Ζηλανδία (Στενό)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Ελβετία';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Πορτογαλία 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Πορτογαλία 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Τσεχία (Στενό)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ΕΕ 433MHz (Μεγάλη Εμβέλεια)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ΕΕ/ΗΒ (Παρωχημένο)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ΕΕ/ΗΒ (Στενό)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ΗΠΑ/Καναδάς (Προτεινόμενο)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Βιετνάμ (Παρωχημένο)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Βιετνάμ (Στενό)';
|
||||
|
||||
@override
|
||||
String get active => 'Ενεργό';
|
||||
|
||||
@override
|
||||
String get addContact => 'Προσθήκη επαφής';
|
||||
|
||||
@override
|
||||
String get all => 'Όλα';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Αυτόματη επίλυση';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Εκκαθάριση όλων';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Εκκαθάριση αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Εκκαθάριση φίλτρων';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Εκκαθάριση διαδρομής';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Εκκαθάριση μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Εκκαθάριση κλίμακας';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Εκκαθάριση ανακαλύψεων';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Εκκαθάριση βάσης ιχνών';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Εκκαθάριση όλων των καναλιών';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Εκκαθάριση όλων των επαφών';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Εκκαθάριση καναλιών';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Εκκαθάριση επαφών';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Εκκαθάριση μονοπατιού στο μέγιστο';
|
||||
|
||||
@override
|
||||
String get create => 'Δημιουργία';
|
||||
|
||||
@override
|
||||
String get custom => 'Προσαρμοσμένο';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Προεπιλογή';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Αντιγραφή';
|
||||
|
||||
@override
|
||||
String get editName => 'Επεξεργασία ονόματος';
|
||||
|
||||
@override
|
||||
String get open => 'Άνοιγμα';
|
||||
|
||||
@override
|
||||
String get paste => 'Επικόλληση';
|
||||
|
||||
@override
|
||||
String get preview => 'Προεπισκόπηση';
|
||||
|
||||
@override
|
||||
String get remove => 'Αφαίρεση';
|
||||
|
||||
@override
|
||||
String get rename => 'Μετονομασία';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Επίλυση όλων';
|
||||
|
||||
@override
|
||||
String get send => 'Αποστολή';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Αποστολή ούτως ή άλλως';
|
||||
|
||||
@override
|
||||
String get share => 'Κοινοποίηση';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Κοινοποίηση επαφής';
|
||||
|
||||
@override
|
||||
String get trace => 'Ίχνος';
|
||||
|
||||
@override
|
||||
String get use => 'Χρήση';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Χρήση επιλεγμένης συχνότητας';
|
||||
|
||||
@override
|
||||
String get discovery => 'Ανακάλυψη';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Ανακάλυψη αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Ανακάλυψη αισθητήρων';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Αποστολή ανακάλυψης αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Αποστολή ανακάλυψης αισθητήρων';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Εκκαθάριση εκκρεμών ανακαλύψεων.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Αυτόματη ανακάλυψη';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Ενεργοποίηση αυτόματης προσθήκης';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Αυτόματη προσθήκη αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Αυτόματη προσθήκη διακομιστών δωματίου';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Αυτόματη προσθήκη αισθητήρων';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Αυτόματη προσθήκη χρηστών';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Αντικατάσταση παλαιότερων όταν γεμίσει';
|
||||
|
||||
@override
|
||||
String get storage => 'Αποθήκευση';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Ζώνη κινδύνου';
|
||||
|
||||
@override
|
||||
String get profiles => 'Προφίλ';
|
||||
|
||||
@override
|
||||
String get favourites => 'Αγαπημένα';
|
||||
|
||||
@override
|
||||
String get sensors => 'Αισθητήρες';
|
||||
|
||||
@override
|
||||
String get others => 'Άλλοι';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Μονάδα GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Ζωντανή κίνηση';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Χάρτης αναμεταδοτών';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Σάρωση φάσματος';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Αρχεία πακέτων BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Βάση δεδομένων ιχνών';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Μέγεθος διαδρομής σε bytes';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Ειδοποιήσεις μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Ειδοποιήσεις SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Ειδοποιήσεις ανακάλυψης';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Ειδοποιήσεις ενημερώσεων';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Σίγαση με ανοιχτή εφαρμογή';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Απενεργοποίηση επαφών';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Ενεργοποίηση καρτέλας Αισθητήρων';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Ενεργοποίηση προφίλ';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Αυτόματη εναλλαγή διαδρομής';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Πλησιέστερος αναμεταδότης ως εφεδρεία';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Διαγραφή όλου του ιστορικού';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Μέγεθος γραμματοσειράς μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Περιστροφή χάρτη με κατεύθυνση';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Εμφάνιση πληροφοριών αποσφαλμάτωσης';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Άνοιγμα χάρτη σε πλήρη οθόνη';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Εμφάνιση δεικτών SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Εμφάνιση δεικτών SAR στον κύριο χάρτη';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Εμφάνιση όλων των ιχνών επαφών';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Απόκρυψη αναμεταδοτών στον χάρτη';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Ρύθμιση κλίμακας χάρτη';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Ρυθμός bit φωνής';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Συμπιεστής φωνής';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Εξισορρόπηση χαμηλών και δυνατών επιπέδων';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Περιοριστής φωνής';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Αποτρέπει κοπή κορυφών πριν την κωδικοποίηση';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Αυτόματη ενίσχυση μικροφώνου';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Αφήνει τη συσκευή να ρυθμίσει την ένταση';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Ακύρωση ηχούς';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Καταστολή θορύβου';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Περικοπή σιωπής σε φωνητικά μηνύματα';
|
||||
|
||||
@override
|
||||
String get compressor => 'Συμπιεστής';
|
||||
|
||||
@override
|
||||
String get limiter => 'Περιοριστής';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Αυτόματη ενίσχυση';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Ηχώ';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Θόρυβος';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Σιωπή';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Μέγιστο μέγεθος εικόνας';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Συμπίεση εικόνας';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Κλίμακα του γκρι';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Λειτουργία ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Γρήγορες ιδιωτικές ενημερώσεις GPS';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Κατώφλι κίνησης';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Κατώφλι κίνησης γρήγορου GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Διάστημα ενεργής χρήσης γρήγορου GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Διάστημα ενημέρωσης ενεργής χρήσης';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Επανάληψη κοντινής κίνησης';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Αναμετάδοση μέσω αναμεταδοτών στο δίκτυο';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Μόνο κοντά, χωρίς πλημμυρισμό';
|
||||
|
||||
@override
|
||||
String get multihop => 'Πολλαπλών αναπηδήσεων';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Δημιουργία προφίλ';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Μετονομασία προφίλ';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Νέο προφίλ';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Διαχείριση προφίλ';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Ενεργοποιήστε τα προφίλ για να τα διαχειριστείτε.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Άνοιγμα μηνύματος';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Μετάβαση στο σχετικό μήνυμα SAR';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Αφαίρεση δείκτη SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Επιλέξτε προορισμό για αποστολή δείκτη SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Δείκτης SAR εκπεμπόμενος στο δημόσιο κανάλι';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Δείκτης SAR εστάλη στο δωμάτιο';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Φόρτωση από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Αντικατάσταση εικόνας';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Επιλογή από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get team => 'Ομάδα';
|
||||
|
||||
@override
|
||||
String get found => 'Βρέθηκε';
|
||||
|
||||
@override
|
||||
String get staging => 'Χώρος συγκέντρωσης';
|
||||
|
||||
@override
|
||||
String get object => 'Αντικείμενο';
|
||||
|
||||
@override
|
||||
String get quiet => 'Ήσυχο';
|
||||
|
||||
@override
|
||||
String get moderate => 'Μέτριο';
|
||||
|
||||
@override
|
||||
String get busy => 'Απασχολημένο';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Η σάρωση φάσματος δεν βρήκε υποψήφιες συχνότητες';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Αναζήτηση μηνυμάτων';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Αποστολή εικόνας από τη γκαλερί';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Λήψη φωτογραφίας';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Μόνο απευθείας';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Όλα τα μηνύματα';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Αποστολή στο δημόσιο κανάλι;';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Επιλέξτε τύπο δείκτη και προορισμό';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel =>
|
||||
'Δεν υπάρχουν διαθέσιμοι προορισμοί';
|
||||
|
||||
@override
|
||||
String get image => 'Εικόνα';
|
||||
|
||||
@override
|
||||
String get format => 'Μορφή';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Διαστάσεις';
|
||||
|
||||
@override
|
||||
String get segments => 'Τμήματα';
|
||||
|
||||
@override
|
||||
String get transfers => 'Μεταφορές';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Λήφθηκε από';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Αποθήκευση ρυθμίσεων ανακάλυψης';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Αποθήκευση δημόσιων πληροφοριών';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Αποθήκευση ρυθμίσεων ραδιοφώνου';
|
||||
|
||||
@override
|
||||
String get savePath => 'Αποθήκευση διαδρομής';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Διαγραφή δεδομένων συσκευής';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Διαγραφή συσκευής';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Καταστροφικές ενέργειες συσκευής.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Επιλέξτε το όνομα και την τοποθεσία που μοιράζεται αυτή η συσκευή.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Διαθέσιμος χώρος σε αυτή τη συσκευή.';
|
||||
|
||||
@override
|
||||
String get used => 'Χρησιμοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get total => 'Σύνολο';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Μετονομασία τιμής';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Προσαρμογή πεδίων';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Ζωντανή προεπισκόπηση';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Χρονοδιάγραμμα ανανέωσης';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Χωρίς απάντηση';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Ανανέωση';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Μη διαθέσιμο';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Δημόσιο κλειδί';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Ήδη στις επαφές';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Από τις επαφές';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Μόνο συνδεδεμένοι';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Σε αμφότερα';
|
||||
|
||||
@override
|
||||
String get source => 'Πηγή';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Χειροκίνητη επεξεργασία διαδρομής';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Παρατηρούμενη διαδρομή δικτύου';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Όλα τα μηνύματα διαγράφηκαν';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Βάση δεδομένων ιχνών εκκαθαρίστηκε';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Τα αρχεία πακέτων εκκαθαρίστηκαν';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Τα δεδομένα hex αντιγράφηκαν';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Λειτουργία προγραμματιστή ενεργοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled =>
|
||||
'Λειτουργία προγραμματιστή απενεργοποιήθηκε';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Το πρόχειρο είναι κενό';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Επαφή εισήχθη';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => 'Ο σύνδεσμος επαφής αντιγράφηκε';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Αποτυχία εξαγωγής επαφής';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Δεν υπάρχουν αρχεία για εξαγωγή';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Εξαγωγή ως CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Εξαγωγή ως κείμενο';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Ελήφθη (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Ώρα κατασκευής';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Η URL λήψης δεν είναι διαθέσιμη';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Αδυναμία ανοίγματος URL λήψης';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Ο έλεγχος ενημερώσεων είναι διαθέσιμος μόνο σε Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Χρησιμοποιείτε την πιο πρόσφατη έκδοση';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Διαθέσιμη ενημέρωση αλλά η URL λήψης δεν βρέθηκε';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Εκκίνηση Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe μη διαθέσιμο';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: άγνωστος αντίπαλος';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: αναμονή για εκκίνηση';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Αποδέχεται κοινόχρηστους συνδέσμους';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Υποστηρίζει ακατέργαστο hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Φιλικό προς πρόχειρο';
|
||||
|
||||
@override
|
||||
String get captured => 'Καταγράφηκε';
|
||||
|
||||
@override
|
||||
String get size => 'Μέγεθος';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Δεν υπάρχουν προσαρμοσμένα κανάλια.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Δεν υπάρχουν επαφές συσκευής.';
|
||||
}
|
||||
|
||||
@@ -1708,4 +1708,958 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Requesting voice';
|
||||
|
||||
@override
|
||||
String get device => 'device';
|
||||
|
||||
@override
|
||||
String get change => 'Change';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'This app combines MeshCore messaging, SAR field updates, mapping, and device tools in one place.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Send direct, room, and channel messages from the main Messages tab.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Share SAR markers, map drawings, voice clips, and images over the mesh.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Connect over BLE or TCP, then manage the companion radio from inside the app.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Messaging and Field Reports';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Messages are more than plain text here. The app already supports several operational payloads and transfer workflows.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Send direct messages, room posts, and channel traffic from one composer.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Create SAR updates and reusable SAR templates for common field reports.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Transfer voice sessions and images, with progress and airtime estimates in the UI.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Connect device';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Connect your MeshCore radio, choose a name, and apply a radio preset before continuing.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Setup';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Overview';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Connected to $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'No device connected yet';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Skip for now';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Device name';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'This name is advertised to other MeshCore users.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Config region';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Uses the full official MeshCore preset list. Default is EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Make sure the selected preset matches your local radio regulations.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'The list matches the official MeshCore config tool preset feed.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) stays selected by default for onboarding.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Saving...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Save and continue';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName => 'Enter a device name before continuing.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Saved $deviceName with $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Contacts, Rooms, and Repeaters';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'The Contacts tab organizes the network you discover and the routes you learn over time.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Review team members, repeaters, rooms, channels, and pending adverts in one list.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Use smart ping, room login, learned paths, and route reset tools when connectivity gets messy.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Create channels and manage network destinations without leaving the app.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Map, Trails, and Shared Geometry';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'The app map is tied directly into messaging, tracking, and SAR overlays instead of being a separate viewer.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Track your own position, teammate locations, and movement trails on the map.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Open drawings from messages, preview them inline, and remove them from the map when needed.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Use repeater map views and shared overlays to understand network reach in the field.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Tools Beyond Messaging';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'There is more here than the four main tabs. The app also includes configuration, diagnostics, and optional sensor workflows.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Open device config to change radio settings, telemetry, TX power, and companion details.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Enable the Sensors tab when you want watched sensor dashboards and quick refresh actions.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Use packet logs, spectrum scan, and developer diagnostics when troubleshooting the mesh.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'In Sensors';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Add to Sensors';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Set path';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName added to Sensors';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Failed to clear route: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Route cleared';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Route set: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Failed to set route: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK timeout';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Payload';
|
||||
|
||||
@override
|
||||
String get hops => 'Hops';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash size';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Path bytes';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Selected path';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Estimated tx';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Sender to receipt';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Received copies';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Retry cause';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Retry mode';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Retry result';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Last retry';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX packets';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Rate';
|
||||
|
||||
@override
|
||||
String get window => 'Window';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Post-tx delay';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Band-pass';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Band-pass filter voice';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequency';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Narrow)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'New Zealand';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'New Zealand (Narrow)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Switzerland';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Czech Republic (Narrow)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Long Range)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (Deprecated)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (Narrow)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Recommended)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Deprecated)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Narrow)';
|
||||
|
||||
@override
|
||||
String get active => 'Active';
|
||||
|
||||
@override
|
||||
String get addContact => 'Add Contact';
|
||||
|
||||
@override
|
||||
String get all => 'All';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Auto resolve';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Clear all';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Clear relays';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Clear filters';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Clear Route';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Clear Messages';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Clear scale';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Clear discoveries';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Clear online trace database';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Clear all channels';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Clear all contacts';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Clear channels';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Clear contacts';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Clear path on max retry';
|
||||
|
||||
@override
|
||||
String get create => 'Create';
|
||||
|
||||
@override
|
||||
String get custom => 'Custom';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Default';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicate';
|
||||
|
||||
@override
|
||||
String get editName => 'Edit name';
|
||||
|
||||
@override
|
||||
String get open => 'Open';
|
||||
|
||||
@override
|
||||
String get paste => 'Paste';
|
||||
|
||||
@override
|
||||
String get preview => 'Preview';
|
||||
|
||||
@override
|
||||
String get remove => 'Remove';
|
||||
|
||||
@override
|
||||
String get rename => 'Rename';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolve all';
|
||||
|
||||
@override
|
||||
String get send => 'Send';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Send anyway';
|
||||
|
||||
@override
|
||||
String get share => 'Share';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Share Contact';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Use';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Use selected frequency';
|
||||
|
||||
@override
|
||||
String get discovery => 'Discovery';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Discover repeaters';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Discover sensors';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Repeater discovery sent';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensor discovery sent';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Cleared pending discoveries.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Auto discovery';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Enable automatic adding';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Auto-add repeaters';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Auto-add room servers';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Auto-add sensors';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Auto-add users';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Overwrite oldest when full';
|
||||
|
||||
@override
|
||||
String get storage => 'Storage';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Danger zone';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profiles';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favourites';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensors';
|
||||
|
||||
@override
|
||||
String get others => 'Others';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS Module';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Live Traffic';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Repeaters Map';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spectrum Scan';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE Packet Logs';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Online trace database';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Route path byte size';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Message notifications';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR alerts';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Discovery notifications';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Update notifications';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Mute while app is open';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Disable Contacts';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Enable Sensors tab';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Enable Profiles';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Auto route rotation';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Nearest repeater fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Delete all stored message history';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Message font size';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotate map with heading';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Show map debug info';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Open map in fullscreen';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Show SAR markers';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Display SAR markers on the main map';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Show all contact trails';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Hide repeaters on map';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Set map scale';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Custom map scale saved';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Voice bitrate';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Voice compressor';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Balances quiet and loud speech levels';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Voice limiter';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Prevents clipping peaks before encoding';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mic auto gain';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Lets the recorder adjust input level';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Echo cancellation';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Noise suppression';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Trim silence in voice messages';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Auto gain';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo cancel';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Noise suppress';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silence trim';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Max image size';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Image compression';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Grayscale';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra mode';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Fast private GPS updates';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Movement threshold';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Fast GPS movement threshold';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Fast GPS active-use interval';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Active-use update interval';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repeat nearby traffic';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Relay through repeaters across the mesh';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Nearby only, without repeater flooding';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Create Profile';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Rename Profile';
|
||||
|
||||
@override
|
||||
String get newProfile => 'New Profile';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Manage profiles';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Enable profiles to start managing them.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Open message';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Jump to the related SAR message';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Remove SAR marker';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Please select a destination to send SAR marker';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR marker broadcast to public channel';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR marker sent to room';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Load from gallery';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Replace image';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Select from gallery';
|
||||
|
||||
@override
|
||||
String get team => 'Team';
|
||||
|
||||
@override
|
||||
String get found => 'Found';
|
||||
|
||||
@override
|
||||
String get staging => 'Staging';
|
||||
|
||||
@override
|
||||
String get object => 'Object';
|
||||
|
||||
@override
|
||||
String get quiet => 'Quiet';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderate';
|
||||
|
||||
@override
|
||||
String get busy => 'Busy';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spectrum scan returned no candidate frequencies';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Search messages';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Send image from gallery';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Take photo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'DM only';
|
||||
|
||||
@override
|
||||
String get allMessages => 'All messages';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Send to Public Channel?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Select marker type and destination';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'No destinations available';
|
||||
|
||||
@override
|
||||
String get image => 'Image';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensions';
|
||||
|
||||
@override
|
||||
String get segments => 'Segments';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transfers';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Downloaded by';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Save discovery settings';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Save public info';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Save radio settings';
|
||||
|
||||
@override
|
||||
String get savePath => 'Save Path';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Wipe device data';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Wipe device';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destructive device actions.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Choose a preset or fine-tune custom radio settings.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Choose the name and location this device shares.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => 'Available space on this device.';
|
||||
|
||||
@override
|
||||
String get used => 'Used';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Rename value';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Customize fields';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Live preview';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Refresh schedule';
|
||||
|
||||
@override
|
||||
String get noResponse => 'No response';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Refreshing';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Unavailable';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Pick a relay or node to watch in Sensors.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Public key';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Already in contacts';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connect to a device before adding contacts';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'From contacts';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Online only';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In both';
|
||||
|
||||
@override
|
||||
String get source => 'Source';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manual route edit';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Observed mesh route';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'All messages cleared';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Online trace database cleared';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Packet logs cleared';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex data copied to clipboard';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Developer mode enabled';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Developer mode disabled';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Clipboard is empty';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contact imported';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => 'Contact link copied to clipboard';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Failed to export contact';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'No logs to export';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Export as CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Export as Text';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Received (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Build Time';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'Download URL not available';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Cannot open download URL';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Update check is only available on Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'You are running the latest version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Update available but download URL not found';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Start Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe unavailable';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: opponent unknown';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: waiting for start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accepts share links';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supports raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Clipboard-friendly';
|
||||
|
||||
@override
|
||||
String get captured => 'Captured';
|
||||
|
||||
@override
|
||||
String get size => 'Size';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'No custom channels to clear.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'No device contacts to clear.';
|
||||
}
|
||||
|
||||
@@ -1736,4 +1736,970 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Solicitando voz';
|
||||
|
||||
@override
|
||||
String get device => 'dispositivo';
|
||||
|
||||
@override
|
||||
String get change => 'Cambiar';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Esta aplicación reúne mensajería MeshCore, actualizaciones SAR de campo, mapas y herramientas del dispositivo en un solo lugar.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Envía mensajes directos, publicaciones en salas y mensajes de canal desde la pestaña principal Mensajes.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Comparte marcadores SAR, dibujos del mapa, clips de voz e imágenes a través de la malla.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Conéctate por BLE o TCP y luego gestiona la radio complementaria desde la aplicación.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Mensajería e informes de campo';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Aquí los mensajes son más que texto plano. La aplicación ya admite varias cargas operativas y flujos de transferencia.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Envía mensajes directos, publicaciones en salas y tráfico de canal desde un solo editor.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Crea actualizaciones SAR y plantillas SAR reutilizables para informes de campo habituales.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Transfiere sesiones de voz e imágenes con progreso y estimaciones de tiempo de aire en la interfaz.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Conectar dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Conecta tu radio MeshCore, elige un nombre y aplica un ajuste predefinido antes de continuar.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Configuración';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Resumen';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Conectado a $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected =>
|
||||
'Aún no hay ningún dispositivo conectado';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Omitir por ahora';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Nombre del dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Este nombre se anuncia a otros usuarios de MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Región de configuración';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Usa la lista oficial completa de ajustes predefinidos de MeshCore. El valor predeterminado es EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Asegúrate de que el ajuste seleccionado coincida con la normativa local de radio.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'La lista coincide con la fuente oficial de ajustes predefinidos de la herramienta de configuración de MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) sigue seleccionado por defecto durante la incorporación.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Guardando...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Guardar y continuar';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Introduce un nombre de dispositivo antes de continuar.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Se guardó $deviceName con $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Contactos, salas y repetidores';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'La pestaña Contactos organiza la red que descubres y las rutas que aprendes con el tiempo.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Revisa miembros del equipo, repetidores, salas, canales y anuncios pendientes en una sola lista.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Usa ping inteligente, acceso a salas, rutas aprendidas y herramientas de reinicio de ruta cuando la conectividad se complique.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Crea canales y gestiona destinos de red sin salir de la aplicación.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Mapa, rastros y geometría compartida';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'El mapa de la aplicación está vinculado directamente a la mensajería, el seguimiento y las superposiciones SAR en lugar de ser un visor aparte.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Sigue tu propia posición, las ubicaciones de tus compañeros y los rastros de movimiento en el mapa.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Abre dibujos desde mensajes, prévisualos en línea y elimínalos del mapa cuando sea necesario.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Usa vistas de mapa de repetidores y superposiciones compartidas para comprender el alcance de la red sobre el terreno.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Herramientas más allá de la mensajería';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Aquí hay más que las cuatro pestañas principales. La aplicación también incluye configuración, diagnóstico y flujos opcionales de sensores.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Abre la configuración del dispositivo para cambiar ajustes de radio, telemetría, potencia TX y detalles del equipo complementario.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Activa la pestaña Sensores cuando quieras paneles de sensores vigilados y acciones de actualización rápida.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Usa registros de paquetes, escaneo de espectro y diagnósticos de desarrollador al solucionar problemas de la malla.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'En Sensores';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Añadir a Sensores';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Definir ruta';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName se añadió a Sensores';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'No se pudo borrar la ruta: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Ruta borrada';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Ruta establecida: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'No se pudo establecer la ruta: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Tiempo de espera ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Código de operación';
|
||||
|
||||
@override
|
||||
String get payload => 'Carga útil';
|
||||
|
||||
@override
|
||||
String get hops => 'Saltos';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Tamaño del hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes de ruta';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Ruta seleccionada';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Transmisión estimada';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Del remitente al recibo';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copias recibidas';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa del reintento';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modo de reintento';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Resultado del reintento';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Último reintento';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Paquetes RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Velocidad';
|
||||
|
||||
@override
|
||||
String get window => 'Ventana';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Retardo post-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Paso de banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro de paso de banda de voz';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frecuencia';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Estrecho)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nueva Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nueva Zelanda (Estrecho)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suiza';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'República Checa (Estrecho)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (Largo Alcance)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Estrecho)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'EE.UU./Canadá (Recomendado)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Estrecho)';
|
||||
|
||||
@override
|
||||
String get active => 'Activo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Añadir contacto';
|
||||
|
||||
@override
|
||||
String get all => 'Todo';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Resolver automáticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Borrar todo';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Borrar repetidores';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Borrar filtros';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Borrar ruta';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Borrar mensajes';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Borrar escala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Borrar descubrimientos';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Borrar base de datos de trazas';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Borrar todos los canales';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Borrar todos los contactos';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Borrar canales';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Borrar contactos';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Borrar ruta en máximo reintento';
|
||||
|
||||
@override
|
||||
String get create => 'Crear';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizado';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Predeterminado';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicar';
|
||||
|
||||
@override
|
||||
String get editName => 'Editar nombre';
|
||||
|
||||
@override
|
||||
String get open => 'Abrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Pegar';
|
||||
|
||||
@override
|
||||
String get preview => 'Vista previa';
|
||||
|
||||
@override
|
||||
String get remove => 'Eliminar';
|
||||
|
||||
@override
|
||||
String get rename => 'Renombrar';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolver todo';
|
||||
|
||||
@override
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Enviar de todos modos';
|
||||
|
||||
@override
|
||||
String get share => 'Compartir';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Compartir contacto';
|
||||
|
||||
@override
|
||||
String get trace => 'Traza';
|
||||
|
||||
@override
|
||||
String get use => 'Usar';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usar frecuencia seleccionada';
|
||||
|
||||
@override
|
||||
String get discovery => 'Descubrimiento';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Descubrir repetidores';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Descubrir sensores';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Descubrimiento de repetidores enviado';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Descubrimiento de sensores enviado';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries =>
|
||||
'Descubrimientos pendientes borrados.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Descubrimiento automático';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Habilitar adición automática';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Añadir repetidores automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Añadir servidores de sala automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Añadir sensores automáticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Añadir usuarios automáticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Sobrescribir más antiguos cuando esté lleno';
|
||||
|
||||
@override
|
||||
String get storage => 'Almacenamiento';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona de peligro';
|
||||
|
||||
@override
|
||||
String get profiles => 'Perfiles';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensores';
|
||||
|
||||
@override
|
||||
String get others => 'Otros';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Módulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Tráfico en vivo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa de repetidores';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Escaneo de espectro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Registros de paquetes BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de datos de trazas';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Tamaño en bytes de la ruta';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notificaciones de mensajes';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertas SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notificaciones de descubrimiento';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notificaciones de actualización';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenciar con la app abierta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Desactivar contactos';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Habilitar pestaña Sensores';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Habilitar perfiles';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotación automática de ruta';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Repetidor más cercano como reserva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Eliminar todo el historial de mensajes';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Tamaño de fuente de mensajes';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotar mapa con rumbo';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostrar info de depuración del mapa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Abrir mapa en pantalla completa';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostrar marcadores SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostrar marcadores SAR en el mapa principal';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Mostrar todos los rastros de contactos';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ocultar repetidores en el mapa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Establecer escala del mapa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Escala de mapa personalizada guardada';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Tasa de bits de voz';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compresor de voz';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Equilibra niveles de habla suaves y altos';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitador de voz';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previene recorte de picos antes de codificar';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Ganancia automática del micrófono';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permite al grabador ajustar el nivel de entrada';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancelación de eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Supresión de ruido';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Recortar silencio en mensajes de voz';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitador';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Ganancia automática';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Ruido';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silencio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Tamaño máximo de imagen';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compresión de imagen';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Escala de grises';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modo ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Actualizaciones GPS privadas rápidas';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Umbral de movimiento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Umbral de movimiento GPS rápido';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervalo de uso activo GPS rápido';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Intervalo de actualización de uso activo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repetir tráfico cercano';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Retransmitir a través de repetidores en la red';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Solo cercanos, sin inundación de repetidores';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-salto';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Crear perfil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renombrar perfil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nuevo perfil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gestionar perfiles';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Habilite los perfiles para comenzar a gestionarlos.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Abrir mensaje';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Ir al mensaje SAR relacionado';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Eliminar marcador SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Seleccione un destino para enviar el marcador SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcador SAR transmitido al canal público';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcador SAR enviado a la sala';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Cargar desde galería';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Reemplazar imagen';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Seleccionar de galería';
|
||||
|
||||
@override
|
||||
String get team => 'Equipo';
|
||||
|
||||
@override
|
||||
String get found => 'Encontrado';
|
||||
|
||||
@override
|
||||
String get staging => 'Área de reunión';
|
||||
|
||||
@override
|
||||
String get object => 'Objeto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silencioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderado';
|
||||
|
||||
@override
|
||||
String get busy => 'Ocupado';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'El escaneo de espectro no encontró frecuencias candidatas';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Buscar mensajes';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Enviar imagen desde galería';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Tomar foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Solo mensaje directo';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Todos los mensajes';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => '¿Enviar al canal público?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Seleccione tipo de marcador y destino';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'No hay destinos disponibles';
|
||||
|
||||
@override
|
||||
String get image => 'Imagen';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensiones';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentos';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferencias';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Descargado por';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Guardar ajustes de descubrimiento';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Guardar información pública';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Guardar ajustes de radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Guardar ruta';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Borrar datos del dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Borrar dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions =>
|
||||
'Acciones destructivas del dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Elija un preajuste o ajuste la configuración de radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Elija el nombre y la ubicación que comparte este dispositivo.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espacio disponible en este dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usado';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renombrar valor';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizar campos';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Vista previa en vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Programación de actualización';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Sin respuesta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Actualizando';
|
||||
|
||||
@override
|
||||
String get unavailable => 'No disponible';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Elija un relay o nodo para observar.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Clave pública';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Ya en contactos';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Conéctese a un dispositivo antes de añadir contactos';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'De contactos';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Solo en línea';
|
||||
|
||||
@override
|
||||
String get inBoth => 'En ambos';
|
||||
|
||||
@override
|
||||
String get source => 'Fuente';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Edición manual de ruta';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Ruta mesh observada';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Todos los mensajes borrados';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de datos de trazas borrada';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Registros de paquetes borrados';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Datos hex copiados al portapapeles';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modo desarrollador activado';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modo desarrollador desactivado';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'El portapapeles está vacío';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contacto importado';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Enlace de contacto copiado al portapapeles';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Error al exportar el contacto';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'No hay registros para exportar';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exportar como CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exportar como texto';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Recibido (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Hora de compilación';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de descarga no disponible';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'No se puede abrir la URL de descarga';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'La comprobación de actualizaciones solo está disponible en Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Está ejecutando la última versión';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Actualización disponible pero URL de descarga no encontrada';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Iniciar Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe no disponible';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: oponente desconocido';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: esperando inicio';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Acepta enlaces compartidos';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Soporta hex sin procesar';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatible con portapapeles';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturado';
|
||||
|
||||
@override
|
||||
String get size => 'Tamaño';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'No hay canales personalizados que borrar.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'No hay contactos del dispositivo que borrar.';
|
||||
}
|
||||
|
||||
@@ -1740,4 +1740,970 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Demande de voix';
|
||||
|
||||
@override
|
||||
String get device => 'appareil';
|
||||
|
||||
@override
|
||||
String get change => 'Modifier';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Cette application réunit la messagerie MeshCore, les mises à jour SAR sur le terrain, la cartographie et les outils de l\'appareil au même endroit.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Envoyez des messages directs, des publications de salon et des messages de canal depuis l\'onglet principal Messages.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Partagez des marqueurs SAR, des dessins de carte, des extraits vocaux et des images sur le réseau maillé.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Connectez-vous en BLE ou en TCP, puis gérez la radio compagnon directement dans l\'application.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Messagerie et rapports de terrain';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Ici, les messages sont plus que du texte brut. L\'application prend déjà en charge plusieurs charges opérationnelles et flux de transfert.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Envoyez des messages directs, des publications de salon et du trafic de canal depuis un seul compositeur.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Créez des mises à jour SAR et des modèles SAR réutilisables pour les rapports de terrain courants.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Transférez des sessions vocales et des images avec la progression et les estimations de temps d\'antenne dans l\'interface.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Connecter l\'appareil';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Connectez votre radio MeshCore, choisissez un nom et appliquez un préréglage radio avant de continuer.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Configuration';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Aperçu';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Connecté à $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected =>
|
||||
'Aucun appareil connecté pour le moment';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Ignorer pour l\'instant';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Nom de l\'appareil';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Ce nom est annoncé aux autres utilisateurs de MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Région de configuration';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Utilise la liste officielle complète des préréglages MeshCore. La valeur par défaut est EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Assurez-vous que le préréglage sélectionné correspond à votre réglementation radio locale.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'La liste correspond au flux officiel des préréglages de l\'outil de configuration MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) reste sélectionné par défaut pendant l\'onboarding.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Enregistrement...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Enregistrer et continuer';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Saisissez un nom d\'appareil avant de continuer.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '$deviceName enregistré avec $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Contacts, salons et répéteurs';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'L\'onglet Contacts organise le réseau que vous découvrez et les routes apprises au fil du temps.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Consultez les membres de l\'équipe, les répéteurs, les salons, les canaux et les annonces en attente dans une seule liste.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Utilisez le smart ping, la connexion aux salons, les chemins appris et les outils de réinitialisation des routes lorsque la connectivité devient chaotique.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Créez des canaux et gérez les destinations réseau sans quitter l\'application.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Carte, traces et géométrie partagée';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'La carte de l\'application est directement liée à la messagerie, au suivi et aux couches SAR au lieu d\'être un simple visualiseur séparé.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Suivez votre position, celles de vos coéquipiers et les traces de déplacement sur la carte.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Ouvrez les dessins depuis les messages, prévisualisez-les en ligne et supprimez-les de la carte si nécessaire.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Utilisez les vues de carte des répéteurs et les couches partagées pour comprendre la portée du réseau sur le terrain.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Outils au-delà de la messagerie';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Il y a plus ici que les quatre onglets principaux. L\'application inclut aussi la configuration, le diagnostic et des workflows de capteurs optionnels.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Ouvrez la configuration de l\'appareil pour modifier les réglages radio, la télémétrie, la puissance TX et les détails du compagnon.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Activez l\'onglet Capteurs lorsque vous voulez des tableaux de bord surveillés et des actions d\'actualisation rapide.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Utilisez les journaux de paquets, le balayage du spectre et les diagnostics développeur pour dépanner le réseau maillé.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Dans Capteurs';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Ajouter aux capteurs';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Définir le chemin';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName ajouté aux capteurs';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Échec de l\'effacement de la route : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Route effacée';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Route définie : $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Échec du réglage de la route : $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Délai ACK expiré';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Charge utile';
|
||||
|
||||
@override
|
||||
String get hops => 'Sauts';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Taille du hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Octets du chemin';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Chemin sélectionné';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Émission estimée';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Émetteur au reçu';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copies reçues';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Cause de la relance';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Mode de relance';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Résultat de la relance';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Dernière relance';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Paquets RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Débit';
|
||||
|
||||
@override
|
||||
String get window => 'Fenêtre';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Délai post-émission';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passe-bande';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtre passe-bande voix';
|
||||
|
||||
@override
|
||||
String get frequency => 'Fréquence';
|
||||
|
||||
@override
|
||||
String get australia => 'Australie';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australie (Étroit)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australie: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australie: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nouvelle-Zélande';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nouvelle-Zélande (Étroit)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suisse';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'République tchèque (Étroit)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Longue Portée)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsolète)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Étroit)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Recommandé)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Obsolète)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Étroit)';
|
||||
|
||||
@override
|
||||
String get active => 'Actif';
|
||||
|
||||
@override
|
||||
String get addContact => 'Ajouter un contact';
|
||||
|
||||
@override
|
||||
String get all => 'Tout';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Résolution automatique';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Tout effacer';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Effacer les relais';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Effacer les filtres';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Effacer la route';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Effacer les messages';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Effacer l\'échelle';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Effacer les découvertes';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Effacer la base de traces';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Effacer tous les canaux';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Effacer tous les contacts';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Effacer les canaux';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Effacer les contacts';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Effacer le chemin au max de relances';
|
||||
|
||||
@override
|
||||
String get create => 'Créer';
|
||||
|
||||
@override
|
||||
String get custom => 'Personnalisé';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Par défaut';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Dupliquer';
|
||||
|
||||
@override
|
||||
String get editName => 'Modifier le nom';
|
||||
|
||||
@override
|
||||
String get open => 'Ouvrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Coller';
|
||||
|
||||
@override
|
||||
String get preview => 'Aperçu';
|
||||
|
||||
@override
|
||||
String get remove => 'Supprimer';
|
||||
|
||||
@override
|
||||
String get rename => 'Renommer';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Résoudre tout';
|
||||
|
||||
@override
|
||||
String get send => 'Envoyer';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Envoyer quand même';
|
||||
|
||||
@override
|
||||
String get share => 'Partager';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Partager le contact';
|
||||
|
||||
@override
|
||||
String get trace => 'Trace';
|
||||
|
||||
@override
|
||||
String get use => 'Utiliser';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Utiliser la fréquence sélectionnée';
|
||||
|
||||
@override
|
||||
String get discovery => 'Découverte';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Découvrir les répéteurs';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Découvrir les capteurs';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Découverte de répéteurs envoyée';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Découverte de capteurs envoyée';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Découvertes en attente effacées.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Découverte automatique';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Activer l\'ajout automatique';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Ajout auto des répéteurs';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Ajout auto des serveurs de salle';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Ajout auto des capteurs';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Ajout auto des utilisateurs';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Écraser les plus anciens si plein';
|
||||
|
||||
@override
|
||||
String get storage => 'Stockage';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zone de danger';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profils';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoris';
|
||||
|
||||
@override
|
||||
String get sensors => 'Capteurs';
|
||||
|
||||
@override
|
||||
String get others => 'Autres';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Module GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Trafic en direct';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Carte des répéteurs';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Scan spectral';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Journaux de paquets BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de données de traces';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Taille en octets du chemin';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notifications de messages';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertes SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notifications de découverte';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notifications de mise à jour';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Muet quand l\'appli est ouverte';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Désactiver les contacts';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Activer l\'onglet Capteurs';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Activer les profils';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotation automatique de route';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Répéteur le plus proche en repli';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Supprimer tout l\'historique';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Taille de police des messages';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Tourner la carte avec le cap';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Afficher les infos de débogage';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Ouvrir la carte en plein écran';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Afficher les marqueurs SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Afficher les marqueurs SAR sur la carte';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Afficher toutes les traces de contacts';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Masquer les répéteurs sur la carte';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Définir l\'échelle de la carte';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved =>
|
||||
'Échelle de carte personnalisée enregistrée';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Débit vocal';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compresseur vocal';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Équilibre les niveaux faibles et forts';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limiteur vocal';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Empêche l\'écrêtage avant l\'encodage';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Gain auto du micro';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Laisse l\'enregistreur ajuster le niveau d\'entrée';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Annulation d\'écho';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Suppression du bruit';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Couper le silence dans les messages vocaux';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compresseur';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiteur';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Gain automatique';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Écho';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Bruit';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silence';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Taille d\'image maximale';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compression d\'image';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Niveaux de gris';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Mode ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Mises à jour GPS privées rapides';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Seuil de mouvement';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Seuil de mouvement GPS rapide';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Intervalle d\'utilisation active GPS rapide';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Intervalle de mise à jour en utilisation active';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Répéter le trafic proche';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Relayer via les répéteurs du réseau';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Proximité seulement, sans inondation';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-saut';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Créer un profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renommer le profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nouveau profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gérer les profils';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Activez les profils pour commencer à les gérer.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Ouvrir le message';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Aller au message SAR associé';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Supprimer le marqueur SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Sélectionnez une destination pour envoyer le marqueur';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marqueur SAR diffusé sur le canal public';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marqueur SAR envoyé à la salle';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Charger depuis la galerie';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Remplacer l\'image';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Sélectionner depuis la galerie';
|
||||
|
||||
@override
|
||||
String get team => 'Équipe';
|
||||
|
||||
@override
|
||||
String get found => 'Trouvé';
|
||||
|
||||
@override
|
||||
String get staging => 'Zone de regroupement';
|
||||
|
||||
@override
|
||||
String get object => 'Objet';
|
||||
|
||||
@override
|
||||
String get quiet => 'Calme';
|
||||
|
||||
@override
|
||||
String get moderate => 'Modéré';
|
||||
|
||||
@override
|
||||
String get busy => 'Occupé';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Le scan spectral n\'a trouvé aucune fréquence candidate';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Rechercher des messages';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Envoyer une image de la galerie';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Prendre une photo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Message direct uniquement';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tous les messages';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Envoyer au canal public ?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Sélectionnez le type de marqueur et la destination';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Aucune destination disponible';
|
||||
|
||||
@override
|
||||
String get image => 'Image';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensions';
|
||||
|
||||
@override
|
||||
String get segments => 'Segments';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferts';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Téléchargé par';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings =>
|
||||
'Enregistrer les paramètres de découverte';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Enregistrer les infos publiques';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Enregistrer les paramètres radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Enregistrer le chemin';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Effacer les données de l\'appareil';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Effacer l\'appareil';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Actions destructives de l\'appareil.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Choisissez un préréglage ou ajustez les paramètres radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Choisissez le nom et l\'emplacement partagés par cet appareil.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espace disponible sur cet appareil.';
|
||||
|
||||
@override
|
||||
String get used => 'Utilisé';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renommer la valeur';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personnaliser les champs';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Aperçu en direct';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Planification d\'actualisation';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Pas de réponse';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Actualisation';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Indisponible';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Choisissez un relais ou nœud à surveiller.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Clé publique';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Déjà dans les contacts';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connectez-vous à un appareil avant d\'ajouter des contacts';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Des contacts';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'En ligne uniquement';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Dans les deux';
|
||||
|
||||
@override
|
||||
String get source => 'Source';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Édition manuelle de route';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Route mesh observée';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tous les messages effacés';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de données de traces effacée';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Journaux de paquets effacés';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Données hex copiées dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Mode développeur activé';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Mode développeur désactivé';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Le presse-papiers est vide';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contact importé';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Lien du contact copié dans le presse-papiers';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Échec de l\'exportation du contact';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Aucun journal à exporter';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exporter en CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exporter en texte';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Reçu (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Heure de build';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de téléchargement non disponible';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl =>
|
||||
'Impossible d\'ouvrir l\'URL de téléchargement';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'La vérification des mises à jour n\'est disponible que sur Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Vous utilisez la dernière version';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Mise à jour disponible mais URL de téléchargement introuvable';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Démarrer Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe indisponible';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: adversaire inconnu';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: en attente du début';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accepte les liens partagés';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supporte le hex brut';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatible avec le presse-papiers';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturé';
|
||||
|
||||
@override
|
||||
String get size => 'Taille';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Aucun canal personnalisé à effacer.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Aucun contact de l\'appareil à effacer.';
|
||||
}
|
||||
|
||||
@@ -1718,4 +1718,961 @@ class AppLocalizationsHr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Zahtjev za glasom';
|
||||
|
||||
@override
|
||||
String get device => 'uređaj';
|
||||
|
||||
@override
|
||||
String get change => 'Promijeni';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Ova aplikacija objedinjuje MeshCore poruke, SAR ažuriranja s terena, karte i alate za uređaj na jednom mjestu.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Šaljite izravne poruke, objave u sobama i poruke kanala iz glavne kartice Poruke.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Dijelite SAR oznake, crteže karte, glasovne isječke i slike preko mesh mreže.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Povežite se putem BLE-a ili TCP-a, a zatim upravljajte pratećim radijem izravno iz aplikacije.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Poruke i terenska izvješća';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Poruke ovdje nisu samo običan tekst. Aplikacija već podržava više operativnih sadržaja i tijekova prijenosa.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Šaljite izravne poruke, objave u sobama i promet kanala iz jednog uređivača.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Stvarajte SAR ažuriranja i višekratne SAR predloške za uobičajena terenska izvješća.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Prenosite glasovne sesije i slike uz prikaz napretka i procjene vremena prijenosa u sučelju.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Poveži uređaj';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Povežite svoj MeshCore radio, odaberite naziv i primijenite radijski predložak prije nastavka.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Postavljanje';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Pregled';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Povezano s uređajem $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Još nema povezanog uređaja';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Preskoči za sada';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Naziv uređaja';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Taj se naziv oglašava drugim korisnicima MeshCorea.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Regija konfiguracije';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Koristi puni službeni popis MeshCore predložaka. Zadana je opcija EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Provjerite odgovara li odabrani predložak lokalnim radijskim propisima.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Popis odgovara službenom izvoru predložaka alata MeshCore config.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) ostaje zadano odabran tijekom uvodnog postavljanja.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Spremanje...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Spremi i nastavi';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName => 'Unesite naziv uređaja prije nastavka.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Spremljeno $deviceName s postavkom $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Kontakti, sobe i repetitori';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Kartica Kontakti organizira mrežu koju otkrijete i rute koje s vremenom naučite.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Pregledajte članove tima, repetitore, sobe, kanale i oglase na čekanju na jednom popisu.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Koristite pametni ping, prijavu u sobe, naučene putanje i alate za resetiranje ruta kada povezanost postane neuredna.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Stvarajte kanale i upravljajte mrežnim odredištima bez napuštanja aplikacije.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Karta, tragovi i dijeljena geometrija';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Karta u aplikaciji izravno je povezana s porukama, praćenjem i SAR slojevima umjesto da bude zaseban preglednik.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Pratite vlastitu poziciju, lokacije suigrača i tragove kretanja na karti.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Otvorite crteže iz poruka, pregledajte ih unutar sučelja i po potrebi ih uklonite s karte.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Koristite karte repetitora i dijeljene slojeve kako biste razumjeli doseg mreže na terenu.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Alati izvan poruka';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Ovdje postoji više od četiri glavne kartice. Aplikacija uključuje i konfiguraciju, dijagnostiku te opcionalne radne tokove senzora.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Otvorite konfiguraciju uređaja kako biste promijenili radijske postavke, telemetriju, TX snagu i detalje pratećeg uređaja.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Omogućite karticu Senzori kada želite nadzorne ploče praćenih senzora i brza osvježavanja.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Koristite zapisnike paketa, skeniranje spektra i razvojnu dijagnostiku pri rješavanju problema s mesh mrežom.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'U Senzorima';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Dodaj u Senzore';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Postavi putanju';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName dodan u Senzore';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Brisanje rute nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Ruta obrisana';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Ruta postavljena: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Postavljanje rute nije uspjelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK istek vremena';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opkod';
|
||||
|
||||
@override
|
||||
String get payload => 'Sadržaj';
|
||||
|
||||
@override
|
||||
String get hops => 'Skokovi';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Veličina hash-a';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajtovi putanje';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Odabrana putanja';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Procijenjeno slanje';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od pošiljatelja do primitka';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Primljene kopije';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Razlog ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Način ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Rezultat ponovnog pokušaja';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Zadnji pokušaj';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketi';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Brzina';
|
||||
|
||||
@override
|
||||
String get window => 'Prozor';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Kašnjenje nakon slanja';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pojasni filtar';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Pojasni filtar glasa';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekvencija';
|
||||
|
||||
@override
|
||||
String get australia => 'Australija';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australija (usko)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australija: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australija: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Novi Zeland';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Novi Zeland (usko)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Švicarska';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Češka (usko)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (veliki domet)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (zastarjelo)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (usko)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'SAD/Kanada (preporučeno)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vijetnam (zastarjelo)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vijetnam (usko)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktivno';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj kontakt';
|
||||
|
||||
@override
|
||||
String get all => 'Sve';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatski razriješi';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Očisti sve';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Očisti posrednike';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Očisti filtre';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Očisti putanju';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Očisti poruke';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Očisti mjerilo';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Očisti otkrića';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Očisti bazu mrežnih tragova';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Očisti sve kanale';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Očisti sve kontakte';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Očisti kanale';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Očisti kontakte';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Očisti putanju kod maks. pokušaja';
|
||||
|
||||
@override
|
||||
String get create => 'Stvori';
|
||||
|
||||
@override
|
||||
String get custom => 'Prilagođeno';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Zadano';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Dupliciraj';
|
||||
|
||||
@override
|
||||
String get editName => 'Uredi ime';
|
||||
|
||||
@override
|
||||
String get open => 'Otvori';
|
||||
|
||||
@override
|
||||
String get paste => 'Zalijepi';
|
||||
|
||||
@override
|
||||
String get preview => 'Pregled';
|
||||
|
||||
@override
|
||||
String get remove => 'Ukloni';
|
||||
|
||||
@override
|
||||
String get rename => 'Preimenuj';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Razriješi sve';
|
||||
|
||||
@override
|
||||
String get send => 'Pošalji';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Svejedno pošalji';
|
||||
|
||||
@override
|
||||
String get share => 'Dijeli';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Dijeli kontakt';
|
||||
|
||||
@override
|
||||
String get trace => 'Praćenje';
|
||||
|
||||
@override
|
||||
String get use => 'Koristi';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Koristi odabranu frekvenciju';
|
||||
|
||||
@override
|
||||
String get discovery => 'Otkrivanje';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Otkrij posrednike';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Otkrij senzore';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Otkrivanje posrednika poslano';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Otkrivanje senzora poslano';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Očišćena čekajuća otkrića.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatsko otkrivanje';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Omogući automatsko dodavanje';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Automatski dodaj posrednike';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Automatski dodaj poslužitelje soba';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Automatski dodaj senzore';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Automatski dodaj korisnike';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Prepiši najstarije kad je puno';
|
||||
|
||||
@override
|
||||
String get storage => 'Pohrana';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Opasna zona';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriti';
|
||||
|
||||
@override
|
||||
String get sensors => 'Senzori';
|
||||
|
||||
@override
|
||||
String get others => 'Ostali';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Promet uživo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Karta posrednika';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektralno skeniranje';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE zapisi paketa';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza mrežnih tragova';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Veličina putanje u bajtima';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Obavijesti o porukama';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR upozorenja';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Obavijesti o otkrivanju';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Obavijesti o ažuriranju';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Utišaj dok je aplikacija otvorena';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Onemogući kontakte';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Omogući karticu Senzori';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Omogući profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatsko rotiranje putanje';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbliži posrednik kao rezerva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Izbriši svu pohranjenu povijest poruka';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Veličina fonta poruka';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rotiraj kartu prema smjeru';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Prikaži debug info karte';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Otvori kartu na cijelom zaslonu';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Prikaži SAR oznake';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Prikaži SAR oznake na glavnoj karti';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Prikaži sve tragove kontakata';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Sakrij posrednike na karti';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Postavi mjerilo karte';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Prilagođeno mjerilo karte spremljeno';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitna brzina glasa';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor glasa';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Izravnava tihe i glasne razine govora';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Ograničivač glasa';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Sprječava rezanje vrhova prije kodiranja';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Automatsko pojačanje mikrofona';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Dopušta snimaču prilagodbu razine ulaza';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Poništavanje odjeka';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Potiskivanje šuma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Izreži tišinu u glasovnim porukama';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Ograničivač';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatsko pojačanje';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Odjek';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Šum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Tišina';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksimalna veličina slike';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Kompresija slike';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Sivine';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra način';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Brza privatna GPS ažuriranja';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Prag kretanja';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Prag kretanja za brzi GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Interval aktivnog korištenja brzog GPS-a';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interval ažuriranja pri aktivnom korištenju';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ponovi obližnji promet';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Proslijedi kroz posrednike kroz mrežu';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Samo obližnji, bez preplavljivanja posrednika';
|
||||
|
||||
@override
|
||||
String get multihop => 'Višeskočno';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Stvori profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Preimenuj profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Novi profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Upravljaj profilima';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Omogućite profile da biste ih počeli upravljati.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Otvori poruku';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Skoči na povezanu SAR poruku';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Ukloni SAR oznaku';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Odaberite odredište za slanje SAR oznake';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR oznaka emitirana na javni kanal';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR oznaka poslana u sobu';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Učitaj iz galerije';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamijeni sliku';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Odaberi iz galerije';
|
||||
|
||||
@override
|
||||
String get team => 'Tim';
|
||||
|
||||
@override
|
||||
String get found => 'Pronađeno';
|
||||
|
||||
@override
|
||||
String get staging => 'Sabirno mjesto';
|
||||
|
||||
@override
|
||||
String get object => 'Objekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Tiho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Umjereno';
|
||||
|
||||
@override
|
||||
String get busy => 'Zauzeto';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektralno skeniranje nije pronašlo prikladne frekvencije';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Pretraži poruke';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Pošalji sliku iz galerije';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Snimi fotografiju';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Samo izravno';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Sve poruke';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Pošalji na javni kanal?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Odaberite vrstu oznake i odredište';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nema dostupnih odredišta';
|
||||
|
||||
@override
|
||||
String get image => 'Slika';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimenzije';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Prijenosi';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Preuzeo';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Spremi postavke otkrivanja';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Spremi javne podatke';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Spremi radijske postavke';
|
||||
|
||||
@override
|
||||
String get savePath => 'Spremi putanju';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Obriši podatke uređaja';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Obriši uređaj';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktivne radnje na uređaju.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Odaberite predefiniranu postavku ili fino podesite radijske postavke.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Odaberite ime i lokaciju koju ovaj uređaj dijeli.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Raspoloživi prostor na ovom uređaju.';
|
||||
|
||||
@override
|
||||
String get used => 'Korišteno';
|
||||
|
||||
@override
|
||||
String get total => 'Ukupno';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Preimenuj vrijednost';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Prilagodi polja';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Pregled uživo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Raspored osvježavanja';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Nema odgovora';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Osvježavam';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nedostupno';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Odaberite posrednika ili čvor za praćenje.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Javni ključ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Već u kontaktima';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Povežite se s uređajem prije dodavanja kontakata';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Iz kontakata';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Samo povezani';
|
||||
|
||||
@override
|
||||
String get inBoth => 'U oba';
|
||||
|
||||
@override
|
||||
String get source => 'Izvor';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ručno uređivanje putanje';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Opažena mrežna putanja';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Sve poruke očišćene';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza mrežnih tragova očišćena';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Zapisi paketa očišćeni';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex podaci kopirani u međuspremnik';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Razvojni način omogućen';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Razvojni način onemogućen';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Međuspremnik je prazan';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt uvezen';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Poveznica na kontakt kopirana u međuspremnik';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Izvoz kontakta nije uspio';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Nema zapisa za izvoz';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Izvezi kao CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Izvezi kao tekst';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Primljeno (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Vrijeme gradnje';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL za preuzimanje nije dostupan';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Ne mogu otvoriti URL za preuzimanje';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Provjera ažuriranja dostupna je samo na Androidu';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Koristite najnoviju verziju';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Ažuriranje dostupno, ali URL za preuzimanje nije pronađen';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Pokreni Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe nije dostupan';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: protivnik nepoznat';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: čekanje na početak';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Prihvaća dijeljene veze';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Podržava sirovi hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Pogodno za međuspremnik';
|
||||
|
||||
@override
|
||||
String get captured => 'Zabilježeno';
|
||||
|
||||
@override
|
||||
String get size => 'Veličina';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Nema prilagođenih kanala za brisanje.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Nema kontakata uređaja za brisanje.';
|
||||
}
|
||||
|
||||
@@ -1732,4 +1732,964 @@ class AppLocalizationsIt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Richiesta voce';
|
||||
|
||||
@override
|
||||
String get device => 'dispositivo';
|
||||
|
||||
@override
|
||||
String get change => 'Modifica';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Questa app riunisce messaggistica MeshCore, aggiornamenti SAR sul campo, mappe e strumenti del dispositivo in un unico posto.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Invia messaggi diretti, post nelle stanze e messaggi di canale dalla scheda principale Messaggi.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Condividi marcatori SAR, disegni sulla mappa, clip vocali e immagini sulla mesh.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Connettiti tramite BLE o TCP e poi gestisci la radio companion direttamente dall\'app.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Messaggistica e rapporti sul campo';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Qui i messaggi sono più del semplice testo. L\'app supporta già diversi payload operativi e flussi di trasferimento.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Invia messaggi diretti, post nelle stanze e traffico di canale da un unico compositore.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Crea aggiornamenti SAR e modelli SAR riutilizzabili per i rapporti sul campo più comuni.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Trasferisci sessioni vocali e immagini, con avanzamento e stime del tempo d\'aria nell\'interfaccia.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Connetti dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Connetti la tua radio MeshCore, scegli un nome e applica un preset radio prima di continuare.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Configurazione';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Panoramica';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Connesso a $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Nessun dispositivo ancora connesso';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Salta per ora';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Nome del dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Questo nome viene annunciato agli altri utenti MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Regione di configurazione';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Usa l\'elenco ufficiale completo dei preset MeshCore. Il valore predefinito è EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Assicurati che il preset selezionato corrisponda alle normative radio locali.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'L\'elenco corrisponde al feed ufficiale dei preset dello strumento di configurazione MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) resta selezionato per impostazione predefinita durante l\'onboarding.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Salvataggio...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Salva e continua';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Inserisci un nome dispositivo prima di continuare.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '$deviceName salvato con $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Contatti, stanze e ripetitori';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'La scheda Contatti organizza la rete che scopri e i percorsi che apprendi nel tempo.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Controlla membri del team, ripetitori, stanze, canali e annunci in sospeso in un unico elenco.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Usa smart ping, accesso alle stanze, percorsi appresi e strumenti di reset delle route quando la connettività diventa complicata.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Crea canali e gestisci le destinazioni di rete senza uscire dall\'app.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Mappa, tracce e geometria condivisa';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'La mappa dell\'app è collegata direttamente a messaggistica, tracciamento e overlay SAR invece di essere un visualizzatore separato.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Monitora la tua posizione, le posizioni dei compagni e le tracce di movimento sulla mappa.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Apri i disegni dai messaggi, visualizzali in anteprima in linea e rimuovili dalla mappa quando serve.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Usa le viste mappa dei ripetitori e gli overlay condivisi per capire la copertura della rete sul campo.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Strumenti oltre la messaggistica';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Qui c\'è più delle quattro schede principali. L\'app include anche configurazione, diagnostica e flussi sensori opzionali.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Apri la configurazione del dispositivo per cambiare impostazioni radio, telemetria, potenza TX e dettagli del companion.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Abilita la scheda Sensori quando vuoi dashboard monitorate e azioni di aggiornamento rapido.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Usa log dei pacchetti, scansione dello spettro e diagnostica sviluppatore per risolvere problemi della mesh.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Nei Sensori';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Aggiungi ai Sensori';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Imposta percorso';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName aggiunto a Sensori';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Impossibile cancellare il percorso: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Percorso cancellato';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Percorso impostato: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Impossibile impostare il percorso: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Timeout ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Payload';
|
||||
|
||||
@override
|
||||
String get hops => 'Hop';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Dimensione hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Byte percorso';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Percorso selezionato';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Trasmissione stimata';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Mittente a ricevuta';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Copie ricevute';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa del ritentativo';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modalità ritentativo';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Risultato ritentativo';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Ultimo ritentativo';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pacchetti RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Velocità';
|
||||
|
||||
@override
|
||||
String get window => 'Finestra';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Ritardo post-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passa-banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro passa-banda voce';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequenza';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (Stretto)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nuova Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nuova Zelanda (Stretto)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Svizzera';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portogallo 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portogallo 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Repubblica Ceca (Stretto)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Lungo Raggio)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/UK (Deprecato)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/UK (Stretto)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Canada (Consigliato)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Deprecato)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Stretto)';
|
||||
|
||||
@override
|
||||
String get active => 'Attivo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Aggiungi contatto';
|
||||
|
||||
@override
|
||||
String get all => 'Tutto';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Risolvi automaticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Cancella tutto';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Cancella relay';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Cancella filtri';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Cancella percorso';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Cancella messaggi';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Cancella scala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Cancella scoperte';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Cancella database tracce';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Cancella tutti i canali';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Cancella tutti i contatti';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Cancella canali';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Cancella contatti';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Cancella percorso al max ritentativo';
|
||||
|
||||
@override
|
||||
String get create => 'Crea';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizzato';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Predefinito';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplica';
|
||||
|
||||
@override
|
||||
String get editName => 'Modifica nome';
|
||||
|
||||
@override
|
||||
String get open => 'Apri';
|
||||
|
||||
@override
|
||||
String get paste => 'Incolla';
|
||||
|
||||
@override
|
||||
String get preview => 'Anteprima';
|
||||
|
||||
@override
|
||||
String get remove => 'Rimuovi';
|
||||
|
||||
@override
|
||||
String get rename => 'Rinomina';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Risolvi tutto';
|
||||
|
||||
@override
|
||||
String get send => 'Invia';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Invia comunque';
|
||||
|
||||
@override
|
||||
String get share => 'Condividi';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Condividi contatto';
|
||||
|
||||
@override
|
||||
String get trace => 'Traccia';
|
||||
|
||||
@override
|
||||
String get use => 'Usa';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usa frequenza selezionata';
|
||||
|
||||
@override
|
||||
String get discovery => 'Scoperta';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Scopri ripetitori';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Scopri sensori';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Scoperta ripetitori inviata';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Scoperta sensori inviata';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Scoperte in attesa cancellate.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Scoperta automatica';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Abilita aggiunta automatica';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Aggiungi ripetitori automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Aggiungi server stanze automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Aggiungi sensori automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Aggiungi utenti automaticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Sovrascrivi i più vecchi quando pieno';
|
||||
|
||||
@override
|
||||
String get storage => 'Archiviazione';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona pericolosa';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Preferiti';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensori';
|
||||
|
||||
@override
|
||||
String get others => 'Altri';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Modulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Traffico in tempo reale';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mappa ripetitori';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Scansione spettro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Log pacchetti BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Database tracce';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Dimensione percorso in byte';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notifiche messaggi';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Avvisi SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notifiche scoperte';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notifiche aggiornamenti';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenzia con l\'app aperta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Disabilita contatti';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Abilita scheda Sensori';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Abilita profili';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotazione automatica percorso';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Ripetitore più vicino come fallback';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Elimina tutta la cronologia messaggi';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Dimensione carattere messaggi';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Ruota mappa con direzione';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostra info debug mappa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Apri mappa a schermo intero';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostra marcatori SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostra marcatori SAR sulla mappa principale';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Mostra tutti i percorsi contatti';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Nascondi ripetitori sulla mappa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Imposta scala mappa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Scala mappa personalizzata salvata';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitrate voce';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compressore voce';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Bilancia i livelli di voce bassi e alti';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitatore voce';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previene il clipping prima della codifica';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Guadagno automatico microfono';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permette al registratore di regolare il livello';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancellazione eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Soppressione rumore';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Taglia silenzio nei messaggi vocali';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressore';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitatore';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Guadagno automatico';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Rumore';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silenzio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Dimensione massima immagine';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compressione immagine';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Scala di grigi';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modalità ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Aggiornamenti GPS privati veloci';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Soglia di movimento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Soglia movimento GPS veloce';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervallo uso attivo GPS veloce';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Intervallo aggiornamento uso attivo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ripeti traffico vicino';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Inoltra tramite ripetitori nella rete';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Solo vicini, senza flood dei ripetitori';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-hop';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Crea profilo';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Rinomina profilo';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nuovo profilo';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gestisci profili';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Abilita i profili per iniziare a gestirli.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Apri messaggio';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Vai al messaggio SAR correlato';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Rimuovi marcatore SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Seleziona una destinazione per inviare il marcatore SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcatore SAR trasmesso al canale pubblico';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcatore SAR inviato alla stanza';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Carica dalla galleria';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Sostituisci immagine';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Seleziona dalla galleria';
|
||||
|
||||
@override
|
||||
String get team => 'Squadra';
|
||||
|
||||
@override
|
||||
String get found => 'Trovato';
|
||||
|
||||
@override
|
||||
String get staging => 'Area di raccolta';
|
||||
|
||||
@override
|
||||
String get object => 'Oggetto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silenzioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderato';
|
||||
|
||||
@override
|
||||
String get busy => 'Occupato';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'La scansione spettro non ha trovato frequenze candidate';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Cerca messaggi';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Invia immagine dalla galleria';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Scatta foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Solo messaggio diretto';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tutti i messaggi';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Inviare al canale pubblico?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Seleziona tipo marcatore e destinazione';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nessuna destinazione disponibile';
|
||||
|
||||
@override
|
||||
String get image => 'Immagine';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensioni';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Trasferimenti';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Scaricato da';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Salva impostazioni scoperta';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Salva info pubbliche';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Salva impostazioni radio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Salva percorso';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Cancella dati dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Cancella dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Azioni distruttive del dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Scegli un preset o personalizza le impostazioni radio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Scegli il nome e la posizione condivisi da questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Spazio disponibile su questo dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usato';
|
||||
|
||||
@override
|
||||
String get total => 'Totale';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Rinomina valore';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizza campi';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Anteprima dal vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Programma aggiornamento';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Nessuna risposta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Aggiornamento';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Non disponibile';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Scegli un relay o nodo da monitorare.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Chiave pubblica';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Già nei contatti';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Connettiti a un dispositivo prima di aggiungere contatti';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Dai contatti';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Solo online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'In entrambi';
|
||||
|
||||
@override
|
||||
String get source => 'Origine';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Modifica manuale percorso';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Percorso mesh osservato';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tutti i messaggi cancellati';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Database tracce cancellata';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Log pacchetti cancellati';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Dati hex copiati negli appunti';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modalità sviluppatore attivata';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modalità sviluppatore disattivata';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Gli appunti sono vuoti';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contatto importato';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link contatto copiato negli appunti';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Esportazione contatto fallita';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Nessun log da esportare';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Esporta come CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Esporta come testo';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Ricevuto (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Ora build';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL di download non disponibile';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Impossibile aprire URL di download';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Il controllo aggiornamenti è disponibile solo su Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Stai utilizzando l\'ultima versione';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Aggiornamento disponibile ma URL di download non trovato';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Avvia Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe non disponibile';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: avversario sconosciuto';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: in attesa dell\'inizio';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Accetta link condivisi';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Supporta hex grezzo';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatibile con appunti';
|
||||
|
||||
@override
|
||||
String get captured => 'Catturato';
|
||||
|
||||
@override
|
||||
String get size => 'Dimensione';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Nessun canale personalizzato da cancellare.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Nessun contatto dispositivo da cancellare.';
|
||||
}
|
||||
|
||||
@@ -1729,4 +1729,964 @@ class AppLocalizationsPl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Pobieranie głosu';
|
||||
|
||||
@override
|
||||
String get device => 'urządzenie';
|
||||
|
||||
@override
|
||||
String get change => 'Zmień';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Ta aplikacja łączy wiadomości MeshCore, terenowe aktualizacje SAR, mapy i narzędzia urządzenia w jednym miejscu.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Wysyłaj wiadomości bezpośrednie, wpisy pokojowe i wiadomości kanałowe z głównej karty Wiadomości.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Udostępniaj znaczniki SAR, rysunki mapy, klipy głosowe i obrazy przez sieć mesh.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Połącz się przez BLE lub TCP, a następnie zarządzaj radiem towarzyszącym bezpośrednio z aplikacji.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Wiadomości i raporty terenowe';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Wiadomości to tutaj coś więcej niż zwykły tekst. Aplikacja obsługuje już kilka typów danych operacyjnych i przepływów transferu.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Wysyłaj wiadomości bezpośrednie, wpisy pokojowe i ruch kanałowy z jednego edytora.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Twórz aktualizacje SAR i wielokrotnego użytku szablony SAR do typowych raportów terenowych.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Przesyłaj sesje głosowe i obrazy z postępem i szacowanym czasem transmisji w interfejsie.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Połącz urządzenie';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Podłącz radio MeshCore, wybierz nazwę i zastosuj profil radiowy przed kontynuacją.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Konfiguracja';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Przegląd';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Połączono z $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Brak podłączonego urządzenia';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Pomiń na razie';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Nazwa urządzenia';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Ta nazwa jest rozgłaszana innym użytkownikom MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Region konfiguracji';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Używa pełnej oficjalnej listy presetów MeshCore. Domyślnie wybrane jest EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Upewnij się, że wybrany preset jest zgodny z lokalnymi przepisami radiowymi.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Lista odpowiada oficjalnemu źródłu presetów narzędzia konfiguracji MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) pozostaje domyślnie wybrane podczas wdrożenia.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Zapisywanie...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Zapisz i kontynuuj';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Wprowadź nazwę urządzenia przed kontynuacją.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Zapisano $deviceName z ustawieniem $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Kontakty, pokoje i repeatery';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Karta Kontakty porządkuje sieć, którą odkrywasz, oraz trasy, których uczysz się z czasem.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Przeglądaj członków zespołu, repeatery, pokoje, kanały i oczekujące ogłoszenia na jednej liście.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Używaj smart ping, logowania do pokoi, poznanych ścieżek i narzędzi resetowania tras, gdy łączność staje się chaotyczna.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Twórz kanały i zarządzaj celami sieciowymi bez opuszczania aplikacji.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Mapa, ślady i współdzielona geometria';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Mapa aplikacji jest bezpośrednio powiązana z wiadomościami, śledzeniem i nakładkami SAR zamiast być osobnym podglądem.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Śledź własną pozycję, lokalizacje członków zespołu i ślady ruchu na mapie.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Otwieraj rysunki z wiadomości, podglądaj je w miejscu i usuwaj z mapy w razie potrzeby.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Korzystaj z widoków map repeaterów i współdzielonych nakładek, aby zrozumieć zasięg sieci w terenie.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Narzędzia poza wiadomościami';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Jest tu więcej niż cztery główne karty. Aplikacja obejmuje także konfigurację, diagnostykę i opcjonalne przepływy czujników.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Otwórz konfigurację urządzenia, aby zmienić ustawienia radia, telemetrię, moc TX i szczegóły urządzenia towarzyszącego.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Włącz kartę Czujniki, gdy chcesz mieć pulpity monitorowanych czujników i szybkie odświeżanie.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Używaj logów pakietów, skanowania widma i diagnostyki deweloperskiej podczas rozwiązywania problemów z mesh.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'W Czujnikach';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Dodaj do Czujników';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Ustaw ścieżkę';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName dodano do Czujników';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Nie udało się wyczyścić trasy: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Trasa wyczyszczona';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Ustawiono trasę: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Nie udało się ustawić trasy: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Limit czasu ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Ładunek';
|
||||
|
||||
@override
|
||||
String get hops => 'Skoki';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Rozmiar hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajty ścieżki';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Wybrana ścieżka';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Szacowane nadawanie';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od nadawcy do odbioru';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Otrzymane kopie';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Przyczyna ponowienia';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Tryb ponowienia';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Wynik ponowienia';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Ostatnia próba';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pakiety RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Szybkość';
|
||||
|
||||
@override
|
||||
String get window => 'Okno';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Opóźnienie po nadawaniu';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pasmowy';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtr pasmowy głosu';
|
||||
|
||||
@override
|
||||
String get frequency => 'Częstotliwość';
|
||||
|
||||
@override
|
||||
String get australia => 'Australia';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Australia (wąski)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Australia: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Australia: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nowa Zelandia';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nowa Zelandia (wąski)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Szwajcaria';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugalia 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugalia 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Czechy (wąski)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (daleki zasięg)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (przestarzały)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (wąski)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'USA/Kanada (zalecany)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Wietnam (przestarzały)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Wietnam (wąski)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktywny';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj kontakt';
|
||||
|
||||
@override
|
||||
String get all => 'Wszystko';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Automatyczne rozwiązanie';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Wyczyść wszystko';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Wyczyść przekaźniki';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Wyczyść filtry';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Wyczyść trasę';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Wyczyść wiadomości';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Wyczyść skalę';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Wyczyść odkrycia';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Wyczyść bazę śladów';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Wyczyść wszystkie kanały';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Wyczyść wszystkie kontakty';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Wyczyść kanały';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Wyczyść kontakty';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Wyczyść ścieżkę przy maks. próbie';
|
||||
|
||||
@override
|
||||
String get create => 'Utwórz';
|
||||
|
||||
@override
|
||||
String get custom => 'Niestandardowy';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Domyślny';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplikuj';
|
||||
|
||||
@override
|
||||
String get editName => 'Edytuj nazwę';
|
||||
|
||||
@override
|
||||
String get open => 'Otwórz';
|
||||
|
||||
@override
|
||||
String get paste => 'Wklej';
|
||||
|
||||
@override
|
||||
String get preview => 'Podgląd';
|
||||
|
||||
@override
|
||||
String get remove => 'Usuń';
|
||||
|
||||
@override
|
||||
String get rename => 'Zmień nazwę';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Rozwiąż wszystko';
|
||||
|
||||
@override
|
||||
String get send => 'Wyślij';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Wyślij mimo to';
|
||||
|
||||
@override
|
||||
String get share => 'Udostępnij';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Udostępnij kontakt';
|
||||
|
||||
@override
|
||||
String get trace => 'Śledzenie';
|
||||
|
||||
@override
|
||||
String get use => 'Użyj';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Użyj wybranej częstotliwości';
|
||||
|
||||
@override
|
||||
String get discovery => 'Wykrywanie';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Wykryj przekaźniki';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Wykryj czujniki';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Wykrywanie przekaźników wysłane';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Wykrywanie czujników wysłane';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Wyczyszczono oczekujące odkrycia.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Automatyczne wykrywanie';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Włącz automatyczne dodawanie';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Automatycznie dodaj przekaźniki';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Automatycznie dodaj serwery pokoi';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Automatycznie dodaj czujniki';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Automatycznie dodaj użytkowników';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Nadpisz najstarsze gdy pełne';
|
||||
|
||||
@override
|
||||
String get storage => 'Pamięć';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Strefa zagrożenia';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profile';
|
||||
|
||||
@override
|
||||
String get favourites => 'Ulubione';
|
||||
|
||||
@override
|
||||
String get sensors => 'Czujniki';
|
||||
|
||||
@override
|
||||
String get others => 'Inne';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Moduł GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Ruch na żywo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa przekaźników';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Skanowanie spektrum';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Logi pakietów BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza śladów';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Rozmiar ścieżki w bajtach';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Powiadomienia o wiadomościach';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alerty SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Powiadomienia o wykrywaniu';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Powiadomienia o aktualizacjach';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Wycisz gdy aplikacja jest otwarta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Wyłącz kontakty';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Włącz kartę Czujniki';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Włącz profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Automatyczna rotacja trasy';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbliższy przekaźnik awaryjny';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Usuń całą historię wiadomości';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Rozmiar czcionki wiadomości';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Obracaj mapę z kierunkiem';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Pokaż info debugowania mapy';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Otwórz mapę na pełnym ekranie';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Pokaż znaczniki SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Wyświetlaj znaczniki SAR na głównej mapie';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Pokaż wszystkie ślady kontaktów';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ukryj przekaźniki na mapie';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Ustaw skalę mapy';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Niestandardowa skala mapy zapisana';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitrate głosu';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor głosu';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Wyrównuje ciche i głośne poziomy mowy';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limiter głosu';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Zapobiega przycinaniu szczytów przed kodowaniem';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Automatyczne wzmocnienie mikrofonu';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Pozwala nagrywarkowi dostosować poziom wejścia';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Usuwanie echa';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Tłumienie szumu';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Przytnij ciszę w wiadomościach głosowych';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limiter';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Automatyczne wzmocnienie';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Echo';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Szum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Cisza';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksymalny rozmiar obrazu';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Kompresja obrazu';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Skala szarości';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Tryb ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Szybkie prywatne aktualizacje GPS';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Próg ruchu';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Próg ruchu szybkiego GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Interwał aktywnego użycia szybkiego GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interwał aktualizacji przy aktywnym użyciu';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Powtarzaj pobliski ruch';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Przekazuj przez przekaźniki w sieci';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Tylko w pobliżu, bez zalewania przekaźników';
|
||||
|
||||
@override
|
||||
String get multihop => 'Wieloskokowy';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Utwórz profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Zmień nazwę profilu';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nowy profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Zarządzaj profilami';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Włącz profile, aby zacząć nimi zarządzać.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Otwórz wiadomość';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Przejdź do powiązanej wiadomości SAR';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Usuń znacznik SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Wybierz cel wysłania znacznika SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Znacznik SAR wysłany na kanał publiczny';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Znacznik SAR wysłany do pokoju';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Załaduj z galerii';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamień obraz';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Wybierz z galerii';
|
||||
|
||||
@override
|
||||
String get team => 'Zespół';
|
||||
|
||||
@override
|
||||
String get found => 'Znaleziono';
|
||||
|
||||
@override
|
||||
String get staging => 'Punkt zbiórki';
|
||||
|
||||
@override
|
||||
String get object => 'Obiekt';
|
||||
|
||||
@override
|
||||
String get quiet => 'Cicho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Umiarkowane';
|
||||
|
||||
@override
|
||||
String get busy => 'Zajęty';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Skanowanie spektrum nie znalazło częstotliwości kandydatów';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Szukaj wiadomości';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Wyślij obraz z galerii';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Zrób zdjęcie';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Tylko wiadomość bezpośrednia';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Wszystkie wiadomości';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Wysłać na kanał publiczny?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => 'Wybierz typ znacznika i cel';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Brak dostępnych celów';
|
||||
|
||||
@override
|
||||
String get image => 'Obraz';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Wymiary';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenty';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transfery';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Pobrane przez';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Zapisz ustawienia wykrywania';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Zapisz informacje publiczne';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Zapisz ustawienia radia';
|
||||
|
||||
@override
|
||||
String get savePath => 'Zapisz ścieżkę';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Wyczyść dane urządzenia';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Wyczyść urządzenie';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destrukcyjne akcje na urządzeniu.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Wybierz ustawienie wstępne lub dostosuj ustawienia radia.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Wybierz nazwę i lokalizację udostępnianą przez to urządzenie.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Dostępne miejsce na tym urządzeniu.';
|
||||
|
||||
@override
|
||||
String get used => 'Użyto';
|
||||
|
||||
@override
|
||||
String get total => 'Razem';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Zmień nazwę wartości';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Dostosuj pola';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Podgląd na żywo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Harmonogram odświeżania';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Brak odpowiedzi';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Odświeżanie';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Niedostępny';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Wybierz przekaźnik lub węzeł do obserwacji.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Klucz publiczny';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Już w kontaktach';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Połącz się z urządzeniem przed dodaniem kontaktów';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Z kontaktów';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Tylko online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'W obu';
|
||||
|
||||
@override
|
||||
String get source => 'Źródło';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ręczna edycja trasy';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Obserwowana trasa sieci';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Wszystkie wiadomości wyczyszczone';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza śladów wyczyszczona';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Logi pakietów wyczyszczone';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Dane hex skopiowane do schowka';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Tryb programisty włączony';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Tryb programisty wyłączony';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Schowek jest pusty';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kontakt zaimportowany';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link kontaktu skopiowany do schowka';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Nie udało się wyeksportować kontaktu';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Brak logów do eksportu';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Eksportuj jako CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Eksportuj jako tekst';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Odebrano (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Czas budowania';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL pobierania niedostępny';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Nie można otworzyć URL pobierania';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Sprawdzanie aktualizacji dostępne tylko na Androidzie';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Używasz najnowszej wersji';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Aktualizacja dostępna, ale URL pobierania nie znaleziony';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Rozpocznij Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe niedostępny';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: przeciwnik nieznany';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: oczekiwanie na start';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Akceptuje udostępnione linki';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Obsługuje surowy hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Przyjazne dla schowka';
|
||||
|
||||
@override
|
||||
String get captured => 'Przechwycone';
|
||||
|
||||
@override
|
||||
String get size => 'Rozmiar';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Brak niestandardowych kanałów do wyczyszczenia.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Brak kontaktów urządzenia do wyczyszczenia.';
|
||||
}
|
||||
|
||||
@@ -1736,4 +1736,968 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Solicitando voz';
|
||||
|
||||
@override
|
||||
String get device => 'dispositivo';
|
||||
|
||||
@override
|
||||
String get change => 'Alterar';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Este aplicativo reúne mensagens MeshCore, atualizações SAR em campo, mapas e ferramentas do dispositivo em um só lugar.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Envie mensagens diretas, publicações em salas e mensagens de canal pela guia principal Mensagens.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Compartilhe marcadores SAR, desenhos de mapa, clipes de voz e imagens pela malha.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Conecte-se via BLE ou TCP e depois gerencie o rádio complementar dentro do aplicativo.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Mensagens e relatórios de campo';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Aqui, as mensagens são mais do que texto simples. O aplicativo já oferece suporte a várias cargas operacionais e fluxos de transferência.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Envie mensagens diretas, publicações em salas e tráfego de canal a partir de um único compositor.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Crie atualizações SAR e modelos SAR reutilizáveis para relatórios de campo comuns.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Transfira sessões de voz e imagens com progresso e estimativas de tempo de transmissão na interface.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Conectar dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Conecte seu rádio MeshCore, escolha um nome e aplique uma predefinição de rádio antes de continuar.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Configuração';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Visão geral';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Conectado a $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Nenhum dispositivo conectado ainda';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Ignorar por enquanto';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Nome do dispositivo';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Esse nome é anunciado para outros usuários do MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Região de configuração';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Usa a lista oficial completa de predefinições do MeshCore. O padrão é EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Certifique-se de que a predefinição selecionada corresponda às regulamentações locais de rádio.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'A lista corresponde ao feed oficial de predefinições da ferramenta de configuração do MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'EU/UK (Narrow) permanece selecionado por padrão durante a configuração inicial.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Salvando...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Salvar e continuar';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Digite um nome para o dispositivo antes de continuar.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '$deviceName salvo com $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Contatos, salas e repetidores';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'A guia Contatos organiza a rede que você descobre e as rotas aprendidas ao longo do tempo.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Revise membros da equipe, repetidores, salas, canais e anúncios pendentes em uma única lista.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Use ping inteligente, login em salas, caminhos aprendidos e ferramentas de redefinição de rota quando a conectividade ficar confusa.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Crie canais e gerencie destinos de rede sem sair do aplicativo.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Mapa, trilhas e geometria compartilhada';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'O mapa do aplicativo está ligado diretamente às mensagens, ao rastreamento e às sobreposições SAR, em vez de ser um visualizador separado.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Acompanhe sua própria posição, as localizações da equipe e as trilhas de movimento no mapa.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Abra desenhos de mensagens, visualize-os em linha e remova-os do mapa quando necessário.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Use vistas de mapa de repetidores e sobreposições compartilhadas para entender o alcance da rede em campo.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Ferramentas além das mensagens';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Há mais aqui do que as quatro guias principais. O aplicativo também inclui configuração, diagnóstico e fluxos opcionais de sensores.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Abra a configuração do dispositivo para alterar ajustes de rádio, telemetria, potência TX e detalhes do equipamento complementar.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Ative a guia Sensores quando quiser painéis monitorados e ações de atualização rápida.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Use logs de pacotes, varredura de espectro e diagnóstico de desenvolvedor ao solucionar problemas da malha.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Nos Sensores';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Adicionar aos Sensores';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Definir rota';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName adicionado a Sensores';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Falha ao limpar a rota: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Rota limpa';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Rota definida: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Falha ao definir a rota: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Tempo limite ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Carga útil';
|
||||
|
||||
@override
|
||||
String get hops => 'Saltos';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Tamanho do hash';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bytes do caminho';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Caminho selecionado';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Transmissão estimada';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Do remetente ao recibo';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Cópias recebidas';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Causa da nova tentativa';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Modo de nova tentativa';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Resultado da nova tentativa';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Última tentativa';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'Pacotes RX';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Taxa';
|
||||
|
||||
@override
|
||||
String get window => 'Janela';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Atraso pós-tx';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Passa-banda';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Filtro passa-banda de voz';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frequência';
|
||||
|
||||
@override
|
||||
String get australia => 'Austrália';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Austrália (Estreito)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Austrália: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Austrália: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nova Zelândia';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nova Zelândia (Estreito)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Suíça';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugal 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugal 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'República Checa (Estreito)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'UE 433MHz (Longo Alcance)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'UE/RU (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'UE/RU (Estreito)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'EUA/Canadá (Recomendado)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietname (Obsoleto)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietname (Estreito)';
|
||||
|
||||
@override
|
||||
String get active => 'Ativo';
|
||||
|
||||
@override
|
||||
String get addContact => 'Adicionar contacto';
|
||||
|
||||
@override
|
||||
String get all => 'Tudo';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Resolver automaticamente';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Limpar tudo';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Limpar relays';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Limpar filtros';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Limpar rota';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Limpar mensagens';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Limpar escala';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Limpar descobertas';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Limpar base de dados de rastreio';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Limpar todos os canais';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Limpar todos os contactos';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Limpar canais';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Limpar contactos';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Limpar caminho no máx. de tentativas';
|
||||
|
||||
@override
|
||||
String get create => 'Criar';
|
||||
|
||||
@override
|
||||
String get custom => 'Personalizado';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Padrão';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Duplicar';
|
||||
|
||||
@override
|
||||
String get editName => 'Editar nome';
|
||||
|
||||
@override
|
||||
String get open => 'Abrir';
|
||||
|
||||
@override
|
||||
String get paste => 'Colar';
|
||||
|
||||
@override
|
||||
String get preview => 'Pré-visualização';
|
||||
|
||||
@override
|
||||
String get remove => 'Remover';
|
||||
|
||||
@override
|
||||
String get rename => 'Renomear';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Resolver tudo';
|
||||
|
||||
@override
|
||||
String get send => 'Enviar';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Enviar mesmo assim';
|
||||
|
||||
@override
|
||||
String get share => 'Partilhar';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Partilhar contacto';
|
||||
|
||||
@override
|
||||
String get trace => 'Rastreio';
|
||||
|
||||
@override
|
||||
String get use => 'Usar';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Usar frequência selecionada';
|
||||
|
||||
@override
|
||||
String get discovery => 'Descoberta';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Descobrir repetidores';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Descobrir sensores';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Descoberta de repetidores enviada';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Descoberta de sensores enviada';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Descobertas pendentes limpas.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Descoberta automática';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Ativar adição automática';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Adicionar repetidores automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers =>
|
||||
'Adicionar servidores de sala automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Adicionar sensores automaticamente';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Adicionar utilizadores automaticamente';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Sobrescrever mais antigos quando cheio';
|
||||
|
||||
@override
|
||||
String get storage => 'Armazenamento';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Zona de perigo';
|
||||
|
||||
@override
|
||||
String get profiles => 'Perfis';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoritos';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensores';
|
||||
|
||||
@override
|
||||
String get others => 'Outros';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Módulo GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Tráfego ao vivo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Mapa de repetidores';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Varredura de espectro';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Registos de pacotes BLE';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Base de dados de rastreio';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Tamanho do caminho em bytes';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Notificações de mensagens';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'Alertas SAR';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Notificações de descoberta';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Notificações de atualização';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Silenciar com a app aberta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Desativar contactos';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Ativar separador Sensores';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Ativar perfis';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Rotação automática de rota';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Repetidor mais próximo como reserva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Apagar todo o histórico de mensagens';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Tamanho da fonte de mensagens';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Rodar mapa com direção';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Mostrar info de depuração do mapa';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Abrir mapa em ecrã inteiro';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Mostrar marcadores SAR';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Mostrar marcadores SAR no mapa principal';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel =>
|
||||
'Mostrar todos os rastos de contactos';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Ocultar repetidores no mapa';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Definir escala do mapa';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Escala de mapa personalizada guardada';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Taxa de bits de voz';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Compressor de voz';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Equilibra níveis de fala suaves e altos';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Limitador de voz';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Previne corte de picos antes da codificação';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Ganho automático do microfone';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Permite ao gravador ajustar o nível de entrada';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Cancelamento de eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Supressão de ruído';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Cortar silêncio em mensagens de voz';
|
||||
|
||||
@override
|
||||
String get compressor => 'Compressor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Limitador';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Ganho automático';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Eco';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Ruído';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Silêncio';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Tamanho máximo de imagem';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Compressão de imagem';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Escala de cinzentos';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Modo ultra';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Atualizações GPS privadas rápidas';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Limiar de movimento';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Limiar de movimento GPS rápido';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Intervalo de uso ativo GPS rápido';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Intervalo de atualização de uso ativo';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Repetir tráfego próximo';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Retransmitir através de repetidores na rede';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Apenas próximos, sem flood de repetidores';
|
||||
|
||||
@override
|
||||
String get multihop => 'Multi-salto';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Criar perfil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Renomear perfil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Novo perfil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Gerir perfis';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Ative os perfis para começar a geri-los.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Abrir mensagem';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Ir para a mensagem SAR relacionada';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Remover marcador SAR';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Selecione um destino para enviar o marcador SAR';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'Marcador SAR transmitido no canal público';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'Marcador SAR enviado para a sala';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Carregar da galeria';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Substituir imagem';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Selecionar da galeria';
|
||||
|
||||
@override
|
||||
String get team => 'Equipa';
|
||||
|
||||
@override
|
||||
String get found => 'Encontrado';
|
||||
|
||||
@override
|
||||
String get staging => 'Área de reunião';
|
||||
|
||||
@override
|
||||
String get object => 'Objeto';
|
||||
|
||||
@override
|
||||
String get quiet => 'Silencioso';
|
||||
|
||||
@override
|
||||
String get moderate => 'Moderado';
|
||||
|
||||
@override
|
||||
String get busy => 'Ocupado';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'A varredura de espectro não encontrou frequências candidatas';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Pesquisar mensagens';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Enviar imagem da galeria';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Tirar foto';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Apenas mensagem direta';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Todas as mensagens';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Enviar para o canal público?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Selecione tipo de marcador e destino';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Nenhum destino disponível';
|
||||
|
||||
@override
|
||||
String get image => 'Imagem';
|
||||
|
||||
@override
|
||||
String get format => 'Formato';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimensões';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentos';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferências';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Transferido por';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Guardar definições de descoberta';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Guardar informação pública';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Guardar definições de rádio';
|
||||
|
||||
@override
|
||||
String get savePath => 'Guardar caminho';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Apagar dados do dispositivo';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Apagar dispositivo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Ações destrutivas do dispositivo.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Escolha uma predefinição ou ajuste as definições de rádio.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Escolha o nome e a localização que este dispositivo partilha.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Espaço disponível neste dispositivo.';
|
||||
|
||||
@override
|
||||
String get used => 'Usado';
|
||||
|
||||
@override
|
||||
String get total => 'Total';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Renomear valor';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Personalizar campos';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Pré-visualização ao vivo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Agenda de atualização';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Sem resposta';
|
||||
|
||||
@override
|
||||
String get refreshing => 'A atualizar';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Indisponível';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Escolha um relay ou nó para observar.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Chave pública';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Já nos contactos';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Conecte-se a um dispositivo antes de adicionar contactos';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Dos contactos';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Apenas online';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Em ambos';
|
||||
|
||||
@override
|
||||
String get source => 'Fonte';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Edição manual de rota';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Rota mesh observada';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Todas as mensagens limpas';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Base de dados de rastreio limpa';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Registos de pacotes limpos';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard =>
|
||||
'Dados hex copiados para a área de transferência';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Modo de programador ativado';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Modo de programador desativado';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'A área de transferência está vazia';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Contacto importado';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Link do contacto copiado para a área de transferência';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Falha ao exportar contacto';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Sem registos para exportar';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Exportar como CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Exportar como texto';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Recebido (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Hora de compilação';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL de download não disponível';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Não é possível abrir o URL de download';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'A verificação de atualizações só está disponível no Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Está a usar a versão mais recente';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Atualização disponível mas URL de download não encontrado';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Iniciar Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe indisponível';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: oponente desconhecido';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: a aguardar início';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Aceita links partilhados';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Suporta hex bruto';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Compatível com a área de transferência';
|
||||
|
||||
@override
|
||||
String get captured => 'Capturado';
|
||||
|
||||
@override
|
||||
String get size => 'Tamanho';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Sem canais personalizados para limpar.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Sem contactos do dispositivo para limpar.';
|
||||
}
|
||||
|
||||
@@ -1726,4 +1726,963 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Запрос голоса';
|
||||
|
||||
@override
|
||||
String get device => 'устройство';
|
||||
|
||||
@override
|
||||
String get change => 'Изменить';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Это приложение объединяет сообщения MeshCore, полевые обновления SAR, карты и инструменты устройства в одном месте.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Отправляйте личные сообщения, сообщения комнат и сообщения каналов из основной вкладки «Сообщения».';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Делитесь SAR-маркерами, рисунками на карте, голосовыми клипами и изображениями через mesh-сеть.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Подключайтесь по BLE или TCP, а затем управляйте сопутствующей радиостанцией прямо из приложения.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Сообщения и полевые отчёты';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Здесь сообщения это больше, чем просто текст. Приложение уже поддерживает несколько видов операционных данных и сценариев передачи.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Отправляйте личные сообщения, сообщения комнат и трафик каналов из одного редактора.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Создавайте обновления SAR и повторно используемые шаблоны SAR для типовых полевых отчётов.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Передавайте голосовые сессии и изображения с индикатором прогресса и оценкой эфирного времени в интерфейсе.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Подключить устройство';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Подключите свою радиостанцию MeshCore, выберите имя и примените радиопрофиль перед продолжением.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Настройка';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Обзор';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Подключено к $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Устройство ещё не подключено';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Пропустить пока';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Имя устройства';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Это имя транслируется другим пользователям MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Регион конфигурации';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Используется полный официальный список профилей MeshCore. По умолчанию выбран EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Убедитесь, что выбранный профиль соответствует местным радиоправилам.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Список соответствует официальному потоку профилей инструмента MeshCore config.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'Для онбординга по умолчанию остаётся выбран EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Сохранение...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Сохранить и продолжить';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Введите имя устройства перед продолжением.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Сохранено: $deviceName, профиль $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Контакты, комнаты и ретрансляторы';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Вкладка «Контакты» организует сеть, которую вы обнаруживаете, и маршруты, которые вы узнаёте со временем.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Просматривайте участников команды, ретрансляторы, комнаты, каналы и ожидающие объявления в одном списке.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Используйте smart ping, вход в комнаты, изученные пути и инструменты сброса маршрутов, когда связь становится нестабильной.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Создавайте каналы и управляйте сетевыми назначениями, не покидая приложение.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Карта, следы и общая геометрия';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Карта приложения напрямую связана с сообщениями, отслеживанием и SAR-слоями, а не является отдельным просмотрщиком.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Отслеживайте свою позицию, местоположение команды и траектории движения на карте.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Открывайте рисунки из сообщений, просматривайте их прямо в интерфейсе и удаляйте с карты при необходимости.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Используйте карты ретрансляторов и общие слои, чтобы понимать покрытие сети в поле.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Инструменты помимо сообщений';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Здесь есть больше, чем четыре основные вкладки. Приложение также включает настройку, диагностику и необязательные сценарии датчиков.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Откройте настройки устройства, чтобы изменить параметры радио, телеметрию, мощность TX и сведения о сопутствующем устройстве.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Включите вкладку «Датчики», если нужны панели наблюдаемых датчиков и быстрые действия обновления.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Используйте журналы пакетов, сканирование спектра и диагностику разработчика для устранения проблем mesh-сети.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'В датчиках';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Добавить в датчики';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Задать путь';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName добавлен в Датчики';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Не удалось очистить маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Маршрут очищен';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Маршрут задан: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Не удалось задать маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Таймаут ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Опкод';
|
||||
|
||||
@override
|
||||
String get payload => 'Полезная нагрузка';
|
||||
|
||||
@override
|
||||
String get hops => 'Хопы';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Размер хеша';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Байты пути';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Выбранный путь';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Ожидаемая передача';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'От отправителя до получения';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Полученные копии';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Причина повтора';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Режим повтора';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Результат повтора';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Последний повтор';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-пакеты';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Скорость';
|
||||
|
||||
@override
|
||||
String get window => 'Окно';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Задержка после передачи';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Полосовой';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Полосовой фильтр голоса';
|
||||
|
||||
@override
|
||||
String get frequency => 'Частота';
|
||||
|
||||
@override
|
||||
String get australia => 'Австралия';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Австралия (Узкий)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Австралия: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Австралия: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Новая Зеландия';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Новая Зеландия (Узкий)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Швейцария';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Португалия 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Португалия 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Чехия (Узкий)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ЕС 433МГц (Дальний)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ЕС/Великобритания (Устарело)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ЕС/Великобритания (Узкий)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'США/Канада (Рекомендуется)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Вьетнам (Устарело)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Вьетнам (Узкий)';
|
||||
|
||||
@override
|
||||
String get active => 'Активно';
|
||||
|
||||
@override
|
||||
String get addContact => 'Добавить контакт';
|
||||
|
||||
@override
|
||||
String get all => 'Все';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Автоматическое разрешение';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Очистить все';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Очистить реле';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Очистить фильтры';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Очистить маршрут';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Очистить сообщения';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Очистить масштаб';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Очистить обнаружения';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Очистить базу трассировок';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Очистить все каналы';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Очистить все контакты';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Очистить каналы';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Очистить контакты';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Очистить путь при макс. повторе';
|
||||
|
||||
@override
|
||||
String get create => 'Создать';
|
||||
|
||||
@override
|
||||
String get custom => 'Пользовательский';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'По умолчанию';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Дублировать';
|
||||
|
||||
@override
|
||||
String get editName => 'Редактировать имя';
|
||||
|
||||
@override
|
||||
String get open => 'Открыть';
|
||||
|
||||
@override
|
||||
String get paste => 'Вставить';
|
||||
|
||||
@override
|
||||
String get preview => 'Предпросмотр';
|
||||
|
||||
@override
|
||||
String get remove => 'Удалить';
|
||||
|
||||
@override
|
||||
String get rename => 'Переименовать';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Разрешить все';
|
||||
|
||||
@override
|
||||
String get send => 'Отправить';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Отправить все равно';
|
||||
|
||||
@override
|
||||
String get share => 'Поделиться';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Поделиться контактом';
|
||||
|
||||
@override
|
||||
String get trace => 'Трассировка';
|
||||
|
||||
@override
|
||||
String get use => 'Использовать';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Использовать выбранную частоту';
|
||||
|
||||
@override
|
||||
String get discovery => 'Обнаружение';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Обнаружить повторители';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Обнаружить датчики';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Обнаружение повторителей отправлено';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Обнаружение датчиков отправлено';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Ожидающие обнаружения очищены.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Автоматическое обнаружение';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Включить автоматическое добавление';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Автодобавление повторителей';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Автодобавление серверов комнат';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Автодобавление датчиков';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Автодобавление пользователей';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Перезаписать старейшие при заполнении';
|
||||
|
||||
@override
|
||||
String get storage => 'Хранилище';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Опасная зона';
|
||||
|
||||
@override
|
||||
String get profiles => 'Профили';
|
||||
|
||||
@override
|
||||
String get favourites => 'Избранное';
|
||||
|
||||
@override
|
||||
String get sensors => 'Датчики';
|
||||
|
||||
@override
|
||||
String get others => 'Другие';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Модуль GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Живой трафик';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Карта повторителей';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Сканирование спектра';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Журналы BLE-пакетов';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'База трассировок';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Размер пути в байтах';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Уведомления о сообщениях';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-оповещения';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Уведомления об обнаружении';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Уведомления об обновлениях';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Без звука при открытом приложении';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Отключить контакты';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Включить вкладку Датчики';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Включить профили';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Автоматическая ротация маршрута';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Ближайший повторитель как резерв';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Удалить всю историю сообщений';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Размер шрифта сообщений';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Вращать карту по направлению';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показать отладочную информацию карты';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Открыть карту на весь экран';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Показать SAR-маркеры';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Отображать SAR-маркеры на основной карте';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Показать все следы контактов';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Скрыть повторители на карте';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Установить масштаб карты';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Пользовательский масштаб карты сохранён';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Битрейт голоса';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Компрессор голоса';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Выравнивает тихую и громкую речь';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Лимитер голоса';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Предотвращает обрезку пиков перед кодированием';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Автоусиление микрофона';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Позволяет рекордеру регулировать уровень входа';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Подавление эха';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Подавление шума';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Обрезать тишину в голосовых сообщениях';
|
||||
|
||||
@override
|
||||
String get compressor => 'Компрессор';
|
||||
|
||||
@override
|
||||
String get limiter => 'Лимитер';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Автоусиление';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Эхо';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Шум';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Тишина';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Максимальный размер изображения';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Сжатие изображения';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Оттенки серого';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Режим ультра';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Быстрые приватные GPS-обновления';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Порог движения';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Порог движения быстрого GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Интервал активного использования быстрого GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Интервал обновления при активном использовании';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Повторять ближний трафик';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ретранслировать через повторители по сети';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Только рядом, без flood повторителей';
|
||||
|
||||
@override
|
||||
String get multihop => 'Многохоповый';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Создать профиль';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Переименовать профиль';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Новый профиль';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Управление профилями';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Включите профили, чтобы начать управлять ими.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Открыть сообщение';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Перейти к связанному SAR-сообщению';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Удалить SAR-маркер';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Выберите назначение для отправки SAR-маркера';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-маркер отправлен в публичный канал';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-маркер отправлен в комнату';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Загрузить из галереи';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Заменить изображение';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Выбрать из галереи';
|
||||
|
||||
@override
|
||||
String get team => 'Команда';
|
||||
|
||||
@override
|
||||
String get found => 'Найдено';
|
||||
|
||||
@override
|
||||
String get staging => 'Место сбора';
|
||||
|
||||
@override
|
||||
String get object => 'Объект';
|
||||
|
||||
@override
|
||||
String get quiet => 'Тихо';
|
||||
|
||||
@override
|
||||
String get moderate => 'Умеренно';
|
||||
|
||||
@override
|
||||
String get busy => 'Занято';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Сканирование спектра не нашло частот-кандидатов';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Поиск сообщений';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Отправить изображение из галереи';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Сделать фото';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Только личные сообщения';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Все сообщения';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Отправить в публичный канал?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Выберите тип маркера и назначение';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Нет доступных назначений';
|
||||
|
||||
@override
|
||||
String get image => 'Изображение';
|
||||
|
||||
@override
|
||||
String get format => 'Формат';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Размеры';
|
||||
|
||||
@override
|
||||
String get segments => 'Сегменты';
|
||||
|
||||
@override
|
||||
String get transfers => 'Передачи';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Загружено';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Сохранить настройки обнаружения';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Сохранить публичную информацию';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Сохранить настройки радио';
|
||||
|
||||
@override
|
||||
String get savePath => 'Сохранить путь';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Стереть данные устройства';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Стереть устройство';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Деструктивные действия устройства.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Выберите пресет или настройте радио вручную.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Выберите имя и местоположение, которыми делится устройство.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Доступное место на этом устройстве.';
|
||||
|
||||
@override
|
||||
String get used => 'Использовано';
|
||||
|
||||
@override
|
||||
String get total => 'Всего';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Переименовать значение';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Настроить поля';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Предпросмотр в реальном времени';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Расписание обновления';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Нет ответа';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Обновление';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Недоступно';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Выберите реле или узел для наблюдения.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Публичный ключ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Уже в контактах';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Подключитесь к устройству перед добавлением контактов';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Из контактов';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Только онлайн';
|
||||
|
||||
@override
|
||||
String get inBoth => 'В обоих';
|
||||
|
||||
@override
|
||||
String get source => 'Источник';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ручное редактирование маршрута';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Наблюдаемый mesh-маршрут';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Все сообщения очищены';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'База трассировок очищена';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Журналы пакетов очищены';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex-данные скопированы в буфер';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Режим разработчика включён';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Режим разработчика отключён';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Буфер обмена пуст';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Контакт импортирован';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Ссылка на контакт скопирована в буфер';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Не удалось экспортировать контакт';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Нет журналов для экспорта';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Экспортировать как CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Экспортировать как текст';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Получено (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Время сборки';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL загрузки недоступен';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Не удаётся открыть URL загрузки';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Проверка обновлений доступна только на Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'Вы используете последнюю версию';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Обновление доступно, но URL загрузки не найден';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Начать Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe недоступно';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: противник неизвестен';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: ожидание начала';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Принимает общие ссылки';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Поддерживает raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Удобно для буфера';
|
||||
|
||||
@override
|
||||
String get captured => 'Захвачено';
|
||||
|
||||
@override
|
||||
String get size => 'Размер';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Нет пользовательских каналов для очистки.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Нет контактов устройства для очистки.';
|
||||
}
|
||||
|
||||
@@ -1718,4 +1718,961 @@ class AppLocalizationsSl extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Zahteva za glasom';
|
||||
|
||||
@override
|
||||
String get device => 'naprava';
|
||||
|
||||
@override
|
||||
String get change => 'Spremeni';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Pošiljajte neposredna sporočila, objave v sobah in sporočila na kanalih iz glavnega zavihka Sporočila.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Delite SAR označevalce, risbe zemljevida, glasovne posnetke in slike prek mesh omrežja.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Povežite se prek BLE ali TCP in nato upravljajte spremljevalni radio kar iz aplikacije.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Sporočanje in terenska poročila';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Sporočila tukaj niso le navadno besedilo. Aplikacija že podpira več operativnih vrst vsebin in prenosnih tokov.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Pošiljajte neposredna sporočila, objave v sobah in promet na kanalih iz enega urejevalnika.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Ustvarjajte SAR posodobitve in večkrat uporabne SAR predloge za pogosta terenska poročila.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Prenašajte glasovne seje in slike, vmesnik pa prikazuje napredek in oceno časa prenosa.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Poveži napravo';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Povežite svoj MeshCore radio, izberite ime in uporabite radijski prednastavljeni profil, preden nadaljujete.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Nastavitev';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Pregled';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Povezano z: $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Nobena naprava še ni povezana';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Za zdaj preskoči';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Ime naprave';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'To ime se oglašuje drugim uporabnikom MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Konfiguracijska regija';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Uporabi celoten uradni seznam prednastavitev MeshCore. Privzeto je izbran EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Prepričajte se, da izbrana prednastavitev ustreza lokalnim radijskim predpisom.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Seznam ustreza uradnemu viru prednastavitev orodja MeshCore config.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'Za uvajanje ostane privzeto izbran EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Shranjujem...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Shrani in nadaljuj';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName => 'Pred nadaljevanjem vnesite ime naprave.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Shranjeno $deviceName z nastavitvijo $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Stiki, sobe in repetitorji';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Zavihek Stiki organizira omrežje, ki ga odkrijete, in poti, ki se jih aplikacija nauči skozi čas.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Na enem seznamu preglejte člane ekipe, repetitorje, sobe, kanale in čakajoče oglase.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Uporabite smart ping, prijavo v sobe, naučene poti in ponastavitev poti, ko povezljivost postane neurejena.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Ustvarjajte kanale in upravljajte omrežne cilje, ne da bi zapustili aplikacijo.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Zemljevid, sledi in deljena geometrija';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Zemljevid v aplikaciji je neposredno povezan s sporočanjem, sledenjem in SAR prekrivanji, namesto da bi bil le ločen pregledovalnik.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Spremljajte svoj položaj, lokacije sotekmovalcev in gibanje sledi na zemljevidu.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Odprite risbe iz sporočil, jih predoglejte v vrstici in jih po potrebi odstranite z zemljevida.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Uporabite poglede repetitorjev in deljena prekrivanja za razumevanje dosega omrežja na terenu.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Orodja onkraj sporočanja';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Tu je več kot le štirje glavni zavihki. Aplikacija vključuje tudi konfiguracijo, diagnostiko in neobvezne senzorske poteke.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Odprite nastavitve naprave za spremembo radijskih nastavitev, telemetrije, oddajne moči in podatkov spremljevalne naprave.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Omogočite zavihek Senzorji, kadar želite nadzorne plošče in hitra osveževanja spremljanih senzorjev.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Uporabite dnevnik paketov, pregled spektra in razvojno diagnostiko pri odpravljanju težav v mesh omrežju.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'V senzorjih';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Dodaj v senzorje';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Nastavi pot';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName dodan v senzorje';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Brisanje poti ni uspelo: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Pot izbrisana';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Pot nastavljena: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Nastavitev poti ni uspela: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Časovna omejitev ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opkoda';
|
||||
|
||||
@override
|
||||
String get payload => 'Vsebina';
|
||||
|
||||
@override
|
||||
String get hops => 'Skoki';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Velikost zgoščevalne vrednosti';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Bajti poti';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Izbrana pot';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Ocenjeni čas oddaje';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Od pošiljatelja do prejema';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Prejete kopije';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Razlog ponovitve';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Način ponovitve';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Rezultat ponovitve';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Zadnja ponovitev';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketi';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Hitrost';
|
||||
|
||||
@override
|
||||
String get window => 'Okno';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Zakasnitev po oddaji';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Pasovno sito';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Pasovno sito za glas';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekvenca';
|
||||
|
||||
@override
|
||||
String get australia => 'Avstralija';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Avstralija (ozko)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Avstralija: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Avstralija: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Nova Zelandija';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Nova Zelandija (ozko)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Švica';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portugalska 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portugalska 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Češka (ozko)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'EU 433MHz (dolg doseg)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'EU/UK (zastarelo)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'EU/UK (ozko)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ZDA/Kanada (priporočeno)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (zastarelo)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (ozko)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktivno';
|
||||
|
||||
@override
|
||||
String get addContact => 'Dodaj stik';
|
||||
|
||||
@override
|
||||
String get all => 'Vse';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Samodejno razreši';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Počisti vse';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Počisti posrednike';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Počisti filtre';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Počisti pot';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Počisti sporočila';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Počisti merilo';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Počisti odkritja';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Počisti bazo sledi';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Počisti vse kanale';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Počisti vse stike';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Počisti kanale';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Počisti stike';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Počisti pot ob maks. ponovitvi';
|
||||
|
||||
@override
|
||||
String get create => 'Ustvari';
|
||||
|
||||
@override
|
||||
String get custom => 'Po meri';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Privzeto';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Podvoji';
|
||||
|
||||
@override
|
||||
String get editName => 'Uredi ime';
|
||||
|
||||
@override
|
||||
String get open => 'Odpri';
|
||||
|
||||
@override
|
||||
String get paste => 'Prilepi';
|
||||
|
||||
@override
|
||||
String get preview => 'Predogled';
|
||||
|
||||
@override
|
||||
String get remove => 'Odstrani';
|
||||
|
||||
@override
|
||||
String get rename => 'Preimenuj';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Razreši vse';
|
||||
|
||||
@override
|
||||
String get send => 'Pošlji';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Vseeno pošlji';
|
||||
|
||||
@override
|
||||
String get share => 'Deli';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Deli stik';
|
||||
|
||||
@override
|
||||
String get trace => 'Sledenje';
|
||||
|
||||
@override
|
||||
String get use => 'Uporabi';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Uporabi izbrano frekvenco';
|
||||
|
||||
@override
|
||||
String get discovery => 'Odkrivanje';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Odkrij posrednike';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Odkrij senzorje';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Odkrivanje posrednikov poslano';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Odkrivanje senzorjev poslano';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Čakalna odkritja počiščena.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Samodejno odkrivanje';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Omogoči samodejno dodajanje';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Samodejno dodaj posrednike';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Samodejno dodaj strežnike sob';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Samodejno dodaj senzorje';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Samodejno dodaj uporabnike';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Prepiši najstarejše ob polnem';
|
||||
|
||||
@override
|
||||
String get storage => 'Shramba';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Nevarno območje';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profili';
|
||||
|
||||
@override
|
||||
String get favourites => 'Priljubljeni';
|
||||
|
||||
@override
|
||||
String get sensors => 'Senzorji';
|
||||
|
||||
@override
|
||||
String get others => 'Ostali';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS modul';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Promet v živo';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Zemljevid posrednikov';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektralno skeniranje';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Dnevniki BLE paketov';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Baza sledi';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Velikost poti v bajtih';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Obvestila o sporočilih';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR opozorila';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Obvestila o odkritjih';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Obvestila o posodobitvah';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Utišaj ko je aplikacija odprta';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Onemogoči stike';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Omogoči zavihek Senzorji';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Omogoči profile';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Samodejno kroženje poti';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Najbližji posrednik kot rezerva';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Izbriši vso shranjeno zgodovino sporočil';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Velikost pisave sporočil';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Vrti zemljevid s smerjo';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Prikaži razhroščevalne info zemljevida';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Odpri zemljevid na cel zaslon';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Prikaži SAR oznake';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Prikaži SAR oznake na glavnem zemljevidu';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Prikaži vse sledi stikov';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Skrij posrednike na zemljevidu';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Nastavi merilo zemljevida';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Merilo zemljevida shranjeno';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Bitna hitrost glasu';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Kompresor glasu';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Izravnava tihe in glasne govorne ravni';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Omejevalnik glasu';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Preprečuje rezanje vrhov pred kodiranjem';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Samodejna ojačitev mikrofona';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Omogoči snemalniku prilagoditev vhodne ravni';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Odstranjevanje odmeva';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Dušenje šuma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Obreži tišino v glasovnih sporočilih';
|
||||
|
||||
@override
|
||||
String get compressor => 'Kompresor';
|
||||
|
||||
@override
|
||||
String get limiter => 'Omejevalnik';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Samodejna ojačitev';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Odmev';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Šum';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Tišina';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Največja velikost slike';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Stiskanje slik';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Sivine';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra način';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Hitri zasebni GPS podatki';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Prag gibanja';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Prag gibanja za hitri GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Interval uporabe hitrega GPS-a';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Interval posodobitve pri aktivni uporabi';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Ponavljaj bližnji promet';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Posreduj skozi posrednike po mreži';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Samo bližnji, brez poplavljanja posrednikov';
|
||||
|
||||
@override
|
||||
String get multihop => 'Večskočno';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Ustvari profil';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Preimenuj profil';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Nov profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Upravljaj profile';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Omogočite profile, da jih začnete upravljati.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Odpri sporočilo';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'Skoči na povezano SAR sporočilo';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Odstrani SAR oznako';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Izberite cilj za pošiljanje SAR oznake';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR oznaka poslana na javni kanal';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR oznaka poslana v sobo';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Naloži iz galerije';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Zamenjaj sliko';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Izberi iz galerije';
|
||||
|
||||
@override
|
||||
String get team => 'Ekipa';
|
||||
|
||||
@override
|
||||
String get found => 'Najdeno';
|
||||
|
||||
@override
|
||||
String get staging => 'Zbirno mesto';
|
||||
|
||||
@override
|
||||
String get object => 'Predmet';
|
||||
|
||||
@override
|
||||
String get quiet => 'Tiho';
|
||||
|
||||
@override
|
||||
String get moderate => 'Zmerno';
|
||||
|
||||
@override
|
||||
String get busy => 'Zasedeno';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektralno skeniranje ni našlo primernih frekvenc';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Iskanje sporočil';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Pošlji sliko iz galerije';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Posnemi fotografijo';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Samo neposredno';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Vsa sporočila';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Pošlji na javni kanal?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => 'Izberite vrsto oznake in cilj';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Ni razpoložljivih ciljev';
|
||||
|
||||
@override
|
||||
String get image => 'Slika';
|
||||
|
||||
@override
|
||||
String get format => 'Format';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Dimenzije';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmenti';
|
||||
|
||||
@override
|
||||
String get transfers => 'Prenosi';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Preneseno od';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Shrani nastavitve odkrivanja';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Shrani javne podatke';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Shrani radijske nastavitve';
|
||||
|
||||
@override
|
||||
String get savePath => 'Shrani pot';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Izbriši podatke naprave';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Izbriši napravo';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Destruktivna dejanja na napravi.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Izberite prednastavitev ali natančno prilagodite radijske nastavitve.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Izberite ime in lokacijo, ki ju naprava deli.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Razpoložljiv prostor na tej napravi.';
|
||||
|
||||
@override
|
||||
String get used => 'Uporabljeno';
|
||||
|
||||
@override
|
||||
String get total => 'Skupaj';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Preimenuj vrednost';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Prilagodi polja';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Predogled v živo';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Interval osvežitve';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Ni odgovora';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Osvežujem';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Nedostopno';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Izberite posrednika ali vozlišče za opazovanje.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Javni ključ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Že med stiki';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Povežite se z napravo pred dodajanjem stikov';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Iz stikov';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Samo povezani';
|
||||
|
||||
@override
|
||||
String get inBoth => 'V obeh';
|
||||
|
||||
@override
|
||||
String get source => 'Vir';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ročno urejanje poti';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Opazovana mrežna pot';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Vsa sporočila počiščena';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Baza sledi počiščena';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Dnevniki paketov počiščeni';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex podatki kopirani v odložišče';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Razvojni način omogočen';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Razvojni način onemogočen';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Odložišče je prazno';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Stik uvožen';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Povezava na stik kopirana v odložišče';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Izvoz stika ni uspel';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Ni dnevnikov za izvoz';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Izvozi kot CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Izvozi kot besedilo';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Prejeto (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Čas gradnje';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL za prenos ni na voljo';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'URL za prenos ni mogoče odpreti';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Preverjanje posodobitev je na voljo le na Androidu';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Uporabljate najnovejšo različico';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Posodobitev na voljo, a URL za prenos ni najden';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Začni Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe ni na voljo';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: nasprotnik neznan';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: čakanje na začetek';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Sprejema deljene povezave';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Podpira surove hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Primerno za odložišče';
|
||||
|
||||
@override
|
||||
String get captured => 'Zajeto';
|
||||
|
||||
@override
|
||||
String get size => 'Velikost';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Ni prilagojenih kanalov za brisanje.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Ni stikov naprave za brisanje.';
|
||||
}
|
||||
|
||||
@@ -1720,4 +1720,957 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Ses isteniyor';
|
||||
|
||||
@override
|
||||
String get device => 'cihaz';
|
||||
|
||||
@override
|
||||
String get change => 'Değiştir';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Bu uygulama MeshCore mesajlaşmasını, SAR saha güncellemelerini, haritalamayı ve cihaz araçlarını tek bir yerde birleştirir.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Ana Mesajlar sekmesinden doğrudan mesajlar, oda gönderileri ve kanal mesajları gönderin.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Mesh üzerinden SAR işaretleri, harita çizimleri, ses klipleri ve görseller paylaşın.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'BLE veya TCP ile bağlanın, ardından yardımcı radyoyu uygulamanın içinden yönetin.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Mesajlaşma ve saha raporları';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Buradaki mesajlar düz metinden fazlasıdır. Uygulama zaten çeşitli operasyonel yükleri ve aktarım akışlarını destekliyor.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Doğrudan mesajları, oda gönderilerini ve kanal trafiğini tek bir düzenleyiciden gönderin.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Yaygın saha raporları için SAR güncellemeleri ve yeniden kullanılabilir SAR şablonları oluşturun.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Arayüzde ilerleme ve yayın süresi tahminleriyle ses oturumları ve görseller aktarın.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Cihazı bağla';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'MeshCore radyonuzu bağlayın, bir ad seçin ve devam etmeden önce bir radyo ön ayarı uygulayın.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Kurulum';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Genel bakış';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return '$deviceName cihazına bağlandı';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Henüz bağlı cihaz yok';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Şimdilik atla';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Cihaz adı';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Bu ad diğer MeshCore kullanıcılarına duyurulur.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Yapılandırma bölgesi';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'MeshCore\'un resmi ön ayar listesinin tamamını kullanır. Varsayılan EU/UK (Narrow) olur.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Seçilen ön ayarın yerel radyo düzenlemelerinize uygun olduğundan emin olun.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Liste, MeshCore yapılandırma aracının resmi ön ayar akışıyla eşleşir.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'Onboarding sırasında varsayılan olarak EU/UK (Narrow) seçili kalır.';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Kaydediliyor...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Kaydet ve devam et';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName => 'Devam etmeden önce bir cihaz adı girin.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '$deviceName, $presetName ile kaydedildi.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Kişiler, odalar ve tekrarlayıcılar';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Kişiler sekmesi keşfettiğiniz ağı ve zamanla öğrendiğiniz rotaları düzenler.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Ekip üyelerini, tekrarlayıcıları, odaları, kanalları ve bekleyen duyuruları tek listede inceleyin.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Bağlantı karmaşıklaştığında smart ping, oda girişi, öğrenilen yollar ve rota sıfırlama araçlarını kullanın.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Uygulamadan çıkmadan kanallar oluşturun ve ağ hedeflerini yönetin.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Harita, izler ve paylaşılan geometri';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Uygulama haritası ayrı bir görüntüleyici olmak yerine mesajlaşma, takip ve SAR katmanlarıyla doğrudan bağlantılıdır.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Kendi konumunuzu, ekip arkadaşlarının konumlarını ve hareket izlerini harita üzerinde takip edin.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Mesajlardaki çizimleri açın, satır içinde önizleyin ve gerektiğinde haritadan kaldırın.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Sahadaki ağ kapsamasını anlamak için tekrarlayıcı harita görünümleri ve paylaşılan katmanları kullanın.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Mesajlaşma dışındaki araçlar';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Burada dört ana sekmeden fazlası var. Uygulama ayrıca yapılandırma, tanılama ve isteğe bağlı sensör akışları içerir.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Radyo ayarlarını, telemetriyi, TX gücünü ve yardımcı cihaz ayrıntılarını değiştirmek için cihaz yapılandırmasını açın.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'İzlenen sensör panoları ve hızlı yenileme eylemleri istediğinizde Sensörler sekmesini etkinleştirin.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Mesh sorunlarını giderirken paket günlükleri, spektrum taraması ve geliştirici tanılamalarını kullanın.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'Sensörlerde';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Sensörlere ekle';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Yol ayarla';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName Sensörlere eklendi';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Rota temizlenemedi: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Rota temizlendi';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Rota ayarlandı: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Rota ayarlanamadı: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK zaman aşımı';
|
||||
|
||||
@override
|
||||
String get opcode => 'Opcode';
|
||||
|
||||
@override
|
||||
String get payload => 'Yük';
|
||||
|
||||
@override
|
||||
String get hops => 'Atlama';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Hash boyutu';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Yol baytları';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Seçilen yol';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Tahmini gönderim';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Gönderenden alındıya';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Alınan kopyalar';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Yeniden deneme nedeni';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Yeniden deneme modu';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Yeniden deneme sonucu';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Son deneme';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX paketleri';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Hız';
|
||||
|
||||
@override
|
||||
String get window => 'Pencere';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Gönderim sonrası gecikme';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Bant geçiren';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Bant geçiren filtre ses';
|
||||
|
||||
@override
|
||||
String get frequency => 'Frekans';
|
||||
|
||||
@override
|
||||
String get australia => 'Avustralya';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Avustralya (Dar)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Avustralya: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Avustralya: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Yeni Zelanda';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Yeni Zelanda (Dar)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'İsviçre';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Portekiz 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Portekiz 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Çek Cumhuriyeti (Dar)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'AB 433MHz (Uzun Menzil)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'AB/BK (Kullanımdan kalktı)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'AB/BK (Dar)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'ABD/Kanada (Önerilen)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'Vietnam (Kullanımdan kalktı)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'Vietnam (Dar)';
|
||||
|
||||
@override
|
||||
String get active => 'Aktif';
|
||||
|
||||
@override
|
||||
String get addContact => 'Kişi ekle';
|
||||
|
||||
@override
|
||||
String get all => 'Tümü';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Otomatik çöz';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Tümünü temizle';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Röleleri temizle';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Filtreleri temizle';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Rotayı temizle';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Mesajları temizle';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Ölçeği temizle';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Keşifleri temizle';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Çevrimiçi iz veritabanını temizle';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Tüm kanalları temizle';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Tüm kişileri temizle';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Kanalları temizle';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Kişileri temizle';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Maks denemede yolu temizle';
|
||||
|
||||
@override
|
||||
String get create => 'Oluştur';
|
||||
|
||||
@override
|
||||
String get custom => 'Özel';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'Varsayılan';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Çoğalt';
|
||||
|
||||
@override
|
||||
String get editName => 'Adı düzenle';
|
||||
|
||||
@override
|
||||
String get open => 'Aç';
|
||||
|
||||
@override
|
||||
String get paste => 'Yapıştır';
|
||||
|
||||
@override
|
||||
String get preview => 'Önizleme';
|
||||
|
||||
@override
|
||||
String get remove => 'Kaldır';
|
||||
|
||||
@override
|
||||
String get rename => 'Yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Tümünü çöz';
|
||||
|
||||
@override
|
||||
String get send => 'Gönder';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Yine de gönder';
|
||||
|
||||
@override
|
||||
String get share => 'Paylaş';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Kişiyi paylaş';
|
||||
|
||||
@override
|
||||
String get trace => 'İz';
|
||||
|
||||
@override
|
||||
String get use => 'Kullan';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Seçilen frekansı kullan';
|
||||
|
||||
@override
|
||||
String get discovery => 'Keşif';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Tekrarlayıcıları keşfet';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Sensörleri keşfet';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Tekrarlayıcı keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Sensör keşfi gönderildi';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Bekleyen keşifler temizlendi.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Otomatik keşif';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Otomatik eklemeyi etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Tekrarlayıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Oda sunucularını otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Sensörleri otomatik ekle';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Kullanıcıları otomatik ekle';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => 'Dolduğunda en eskiyi üzerine yaz';
|
||||
|
||||
@override
|
||||
String get storage => 'Depolama';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Tehlikeli bölge';
|
||||
|
||||
@override
|
||||
String get profiles => 'Profiller';
|
||||
|
||||
@override
|
||||
String get favourites => 'Favoriler';
|
||||
|
||||
@override
|
||||
String get sensors => 'Sensörler';
|
||||
|
||||
@override
|
||||
String get others => 'Diğerleri';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS Modülü';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Canlı trafik';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Tekrarlayıcılar haritası';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Spektrum taraması';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE paket günlükleri';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'Çevrimiçi iz veritabanı';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Yol bayt boyutu';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Mesaj bildirimleri';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR uyarıları';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Keşif bildirimleri';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Güncelleme bildirimleri';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Uygulama açıkken sessize al';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Kişileri devre dışı bırak';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Sensörler sekmesini etkinleştir';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Profilleri etkinleştir';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Otomatik rota rotasyonu';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'En yakın tekrarlayıcıya geri dönüş';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => 'Tüm mesaj geçmişini sil';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Mesaj yazı tipi boyutu';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Haritayı yön ile döndür';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Harita hata ayıklama bilgisini göster';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Haritayı tam ekranda aç';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'SAR işaretçilerini göster';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'SAR işaretçilerini ana haritada göster';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Tüm kişi izlerini göster';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Haritada tekrarlayıcıları gizle';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Harita ölçeğini ayarla';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Özel harita ölçeği kaydedildi';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Ses bit hızı';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Ses sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Sessiz ve yüksek konuşma seviyelerini dengeler';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Ses sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Kodlama öncesi tepe kırpmasını önler';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Mikrofon otomatik kazancı';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Kaydedicinin giriş seviyesini ayarlamasına izin verir';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Yankı iptali';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Gürültü bastırma';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => 'Sesli mesajlarda sessizliği kırp';
|
||||
|
||||
@override
|
||||
String get compressor => 'Sıkıştırıcı';
|
||||
|
||||
@override
|
||||
String get limiter => 'Sınırlayıcı';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Otomatik kazanç';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Yankı';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Gürültü';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Sessizlik';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Maksimum resim boyutu';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Resim sıkıştırma';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Gri tonlama';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ultra mod';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Hızlı özel GPS güncellemeleri';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Hızlı GPS hareket eşiği';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => 'Hızlı GPS aktif kullanım aralığı';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => 'Aktif kullanım güncelleme aralığı';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Yakın trafiği tekrarla';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Yalnızca yakın, tekrarlayıcı taşkını olmadan';
|
||||
|
||||
@override
|
||||
String get multihop => 'Çok atlamalı';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Profil oluştur';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Profili yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Yeni profil';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Profilleri yönet';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Profilleri yönetmeye başlamak için etkinleştirin.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Mesajı aç';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => 'İlgili SAR mesajına git';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'SAR işaretçisini kaldır';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'SAR işaretçisi göndermek için bir hedef seçin';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR işaretçisi genel kanala yayınlandı';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR işaretçisi odaya gönderildi';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Galeriden yükle';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Resmi değiştir';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Galeriden seç';
|
||||
|
||||
@override
|
||||
String get team => 'Takım';
|
||||
|
||||
@override
|
||||
String get found => 'Bulundu';
|
||||
|
||||
@override
|
||||
String get staging => 'Toplanma alanı';
|
||||
|
||||
@override
|
||||
String get object => 'Nesne';
|
||||
|
||||
@override
|
||||
String get quiet => 'Sessiz';
|
||||
|
||||
@override
|
||||
String get moderate => 'Orta';
|
||||
|
||||
@override
|
||||
String get busy => 'Meşgul';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Spektrum taraması aday frekans bulamadı';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Mesajlarda ara';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Galeriden resim gönder';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Fotoğraf çek';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Yalnızca DM';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Tüm mesajlar';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Genel kanala gönderilsin mi?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'İşaretçi türünü ve hedefi seçin';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Kullanılabilir hedef yok';
|
||||
|
||||
@override
|
||||
String get image => 'Resim';
|
||||
|
||||
@override
|
||||
String get format => 'Biçim';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Boyutlar';
|
||||
|
||||
@override
|
||||
String get segments => 'Segmentler';
|
||||
|
||||
@override
|
||||
String get transfers => 'Transferler';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'İndiren';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Keşif ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Genel bilgileri kaydet';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Radyo ayarlarını kaydet';
|
||||
|
||||
@override
|
||||
String get savePath => 'Yolu kaydet';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Cihaz verilerini sil';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Cihazı sil';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Yıkıcı cihaz işlemleri.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Bu cihazın paylaştığı ad ve konumu seçin.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => 'Bu cihazda kullanılabilir alan.';
|
||||
|
||||
@override
|
||||
String get used => 'Kullanılan';
|
||||
|
||||
@override
|
||||
String get total => 'Toplam';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Değeri yeniden adlandır';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Alanları özelleştir';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Canlı önizleme';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Yenileme programı';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Yanıt yok';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Yenileniyor';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Kullanılamaz';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'İzlemek için bir röle veya düğüm seçin.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Genel anahtar';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Zaten kişilerde';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Kişi eklemeden önce bir cihaza bağlanın';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'Kişilerden';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Yalnızca çevrimiçi';
|
||||
|
||||
@override
|
||||
String get inBoth => 'Her ikisinde';
|
||||
|
||||
@override
|
||||
String get source => 'Kaynak';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Manuel rota düzenle';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Gözlemlenen mesh rotası';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Tüm mesajlar temizlendi';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Çevrimiçi iz veritabanı temizlendi';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Paket günlükleri temizlendi';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex verisi panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Geliştirici modu etkinleştirildi';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Geliştirici modu devre dışı bırakıldı';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Pano boş';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Kişi içe aktarıldı';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Kişi bağlantısı panoya kopyalandı';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Kişi dışa aktarılamadı';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Dışa aktarılacak günlük yok';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'CSV olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Metin olarak dışa aktar';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Alındı (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Derleme zamanı';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'İndirme URL\'si mevcut değil';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'İndirme URL\'si açılamıyor';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Güncelleme kontrolü yalnızca Android\'de kullanılabilir';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => 'En son sürümü kullanıyorsunuz';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Güncelleme mevcut ancak indirme URL\'si bulunamadı';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Tic-Tac-Toe başlat';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe kullanılamıyor';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: rakip bilinmiyor';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: başlangıç bekleniyor';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Paylaşım bağlantılarını kabul eder';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Ham hex destekler';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Pano dostu';
|
||||
|
||||
@override
|
||||
String get captured => 'Yakalandı';
|
||||
|
||||
@override
|
||||
String get size => 'Boyut';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => 'Temizlenecek özel kanal yok.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => 'Temizlenecek cihaz kişisi yok.';
|
||||
}
|
||||
|
||||
@@ -1732,4 +1732,968 @@ class AppLocalizationsUk extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => 'Запит голосу';
|
||||
|
||||
@override
|
||||
String get device => 'пристрій';
|
||||
|
||||
@override
|
||||
String get change => 'Змінити';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'Ця програма поєднує повідомлення MeshCore, польові оновлення SAR, карти та інструменти пристрою в одному місці.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 =>
|
||||
'Надсилайте особисті повідомлення, повідомлення кімнат і повідомлення каналів з основної вкладки «Повідомлення».';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 =>
|
||||
'Діліться SAR-маркерами, малюнками на карті, голосовими кліпами та зображеннями через mesh-мережу.';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 =>
|
||||
'Підключайтеся через BLE або TCP, а потім керуйте супутнім радіопристроєм прямо з програми.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => 'Повідомлення та польові звіти';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription =>
|
||||
'Повідомлення тут це більше, ніж просто текст. Програма вже підтримує кілька типів операційних даних і сценаріїв передачі.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 =>
|
||||
'Надсилайте особисті повідомлення, повідомлення кімнат і трафік каналів з одного редактора.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 =>
|
||||
'Створюйте оновлення SAR і багаторазові шаблони SAR для типових польових звітів.';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 =>
|
||||
'Передавайте голосові сесії та зображення з індикатором прогресу й оцінками ефірного часу в інтерфейсі.';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => 'Підключити пристрій';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'Підключіть своє радіо MeshCore, виберіть назву та застосуйте радіопрофіль перед продовженням.';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => 'Налаштування';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => 'Огляд';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return 'Підключено до $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => 'Ще немає підключеного пристрою';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => 'Пропустити поки що';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => 'Назва пристрою';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp =>
|
||||
'Ця назва оголошується іншим користувачам MeshCore.';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => 'Регіон конфігурації';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'Використовується повний офіційний список профілів MeshCore. Типово вибрано EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 =>
|
||||
'Переконайтеся, що вибраний профіль відповідає місцевим радіоправилам.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 =>
|
||||
'Список відповідає офіційному потоку профілів інструмента MeshCore config.';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 =>
|
||||
'Для онбордингу типовим залишається EU/UK (Narrow).';
|
||||
|
||||
@override
|
||||
String get wizardSaving => 'Збереження...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => 'Зберегти й продовжити';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName =>
|
||||
'Введіть назву пристрою перед продовженням.';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return 'Збережено $deviceName з профілем $presetName.';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => 'Контакти, кімнати та ретранслятори';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription =>
|
||||
'Вкладка «Контакти» організовує мережу, яку ви виявляєте, і маршрути, які вивчаєте з часом.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 =>
|
||||
'Переглядайте членів команди, ретранслятори, кімнати, канали та оголошення в очікуванні в одному списку.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 =>
|
||||
'Використовуйте smart ping, вхід до кімнат, вивчені шляхи й інструменти скидання маршрутів, коли зв\'язок стає нестабільним.';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 =>
|
||||
'Створюйте канали та керуйте мережевими призначеннями, не виходячи з програми.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => 'Мапа, сліди та спільна геометрія';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription =>
|
||||
'Мапа програми напряму пов\'язана з повідомленнями, відстеженням і SAR-накладками, а не є окремим переглядачем.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 =>
|
||||
'Відстежуйте власну позицію, місця розташування команди та сліди руху на мапі.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 =>
|
||||
'Відкривайте малюнки з повідомлень, переглядайте їх в інтерфейсі та видаляйте з мапи за потреби.';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 =>
|
||||
'Використовуйте мапи ретрансляторів і спільні накладки, щоб розуміти покриття мережі в полі.';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => 'Інструменти поза повідомленнями';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription =>
|
||||
'Тут є більше, ніж чотири основні вкладки. Програма також включає налаштування, діагностику та необов\'язкові сценарії датчиків.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 =>
|
||||
'Відкрийте налаштування пристрою, щоб змінити параметри радіо, телеметрію, потужність TX і дані супутнього пристрою.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 =>
|
||||
'Увімкніть вкладку «Датчики», коли потрібні панелі моніторингу та швидкі дії оновлення.';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 =>
|
||||
'Використовуйте журнали пакетів, сканування спектра та діагностику розробника для усунення проблем mesh-мережі.';
|
||||
|
||||
@override
|
||||
String get contactInSensors => 'У датчиках';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => 'Додати до датчиків';
|
||||
|
||||
@override
|
||||
String get contactSetPath => 'Задати шлях';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '$contactName додано до Датчиків';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return 'Не вдалося очистити маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => 'Маршрут очищено';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return 'Маршрут задано: $route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return 'Не вдалося задати маршрут: $error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'Тайм-аут ACK';
|
||||
|
||||
@override
|
||||
String get opcode => 'Опкод';
|
||||
|
||||
@override
|
||||
String get payload => 'Корисне навантаження';
|
||||
|
||||
@override
|
||||
String get hops => 'Хопи';
|
||||
|
||||
@override
|
||||
String get hashSize => 'Розмір хешу';
|
||||
|
||||
@override
|
||||
String get pathBytes => 'Байти шляху';
|
||||
|
||||
@override
|
||||
String get selectedPath => 'Обраний шлях';
|
||||
|
||||
@override
|
||||
String get estimatedTx => 'Очікувана передача';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => 'Від відправника до отримання';
|
||||
|
||||
@override
|
||||
String get receivedCopies => 'Отримані копії';
|
||||
|
||||
@override
|
||||
String get retryCause => 'Причина повтору';
|
||||
|
||||
@override
|
||||
String get retryMode => 'Режим повтору';
|
||||
|
||||
@override
|
||||
String get retryResult => 'Результат повтору';
|
||||
|
||||
@override
|
||||
String get lastRetry => 'Остання спроба';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX-пакети';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => 'Швидкість';
|
||||
|
||||
@override
|
||||
String get window => 'Вікно';
|
||||
|
||||
@override
|
||||
String get posttxDelay => 'Затримка після передачі';
|
||||
|
||||
@override
|
||||
String get bandpass => 'Смуговий';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => 'Смуговий фільтр голосу';
|
||||
|
||||
@override
|
||||
String get frequency => 'Частота';
|
||||
|
||||
@override
|
||||
String get australia => 'Австралія';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => 'Австралія (Вузький)';
|
||||
|
||||
@override
|
||||
String get australiaQld => 'Австралія: QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => 'Австралія: SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => 'Нова Зеландія';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => 'Нова Зеландія (Вузький)';
|
||||
|
||||
@override
|
||||
String get switzerland => 'Швейцарія';
|
||||
|
||||
@override
|
||||
String get portugal433 => 'Португалія 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => 'Португалія 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => 'Чехія (Вузький)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => 'ЄС 433МГц (Далекий)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => 'ЄС/Великобританія (Застаріле)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => 'ЄС/Великобританія (Вузький)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => 'США/Канада (Рекомендовано)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => 'В\'єтнам (Застаріле)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => 'В\'єтнам (Вузький)';
|
||||
|
||||
@override
|
||||
String get active => 'Активно';
|
||||
|
||||
@override
|
||||
String get addContact => 'Додати контакт';
|
||||
|
||||
@override
|
||||
String get all => 'Усі';
|
||||
|
||||
@override
|
||||
String get autoResolve => 'Автоматичне вирішення';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => 'Очистити все';
|
||||
|
||||
@override
|
||||
String get clearRelays => 'Очистити реле';
|
||||
|
||||
@override
|
||||
String get clearFilters => 'Очистити фільтри';
|
||||
|
||||
@override
|
||||
String get clearRoute => 'Очистити маршрут';
|
||||
|
||||
@override
|
||||
String get clearMessages => 'Очистити повідомлення';
|
||||
|
||||
@override
|
||||
String get clearScale => 'Очистити масштаб';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => 'Очистити виявлення';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => 'Очистити базу трасувань';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => 'Очистити всі канали';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => 'Очистити всі контакти';
|
||||
|
||||
@override
|
||||
String get clearChannels => 'Очистити канали';
|
||||
|
||||
@override
|
||||
String get clearContacts => 'Очистити контакти';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => 'Очистити шлях при макс. спробі';
|
||||
|
||||
@override
|
||||
String get create => 'Створити';
|
||||
|
||||
@override
|
||||
String get custom => 'Користувацький';
|
||||
|
||||
@override
|
||||
String get defaultValue => 'За замовчуванням';
|
||||
|
||||
@override
|
||||
String get duplicate => 'Дублювати';
|
||||
|
||||
@override
|
||||
String get editName => 'Редагувати ім\'я';
|
||||
|
||||
@override
|
||||
String get open => 'Відкрити';
|
||||
|
||||
@override
|
||||
String get paste => 'Вставити';
|
||||
|
||||
@override
|
||||
String get preview => 'Попередній перегляд';
|
||||
|
||||
@override
|
||||
String get remove => 'Видалити';
|
||||
|
||||
@override
|
||||
String get rename => 'Перейменувати';
|
||||
|
||||
@override
|
||||
String get resolveAll => 'Вирішити все';
|
||||
|
||||
@override
|
||||
String get send => 'Надіслати';
|
||||
|
||||
@override
|
||||
String get sendAnyway => 'Надіслати все одно';
|
||||
|
||||
@override
|
||||
String get share => 'Поділитися';
|
||||
|
||||
@override
|
||||
String get shareContact => 'Поділитися контактом';
|
||||
|
||||
@override
|
||||
String get trace => 'Трасування';
|
||||
|
||||
@override
|
||||
String get use => 'Використати';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => 'Використати обрану частоту';
|
||||
|
||||
@override
|
||||
String get discovery => 'Виявлення';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => 'Виявити повторювачі';
|
||||
|
||||
@override
|
||||
String get discoverSensors => 'Виявити датчики';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => 'Виявлення повторювачів надіслано';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => 'Виявлення датчиків надіслано';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => 'Очікувані виявлення очищено.';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => 'Автоматичне виявлення';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => 'Увімкнути автоматичне додавання';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => 'Автододавання повторювачів';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => 'Автододавання серверів кімнат';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => 'Автододавання датчиків';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => 'Автододавання користувачів';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull =>
|
||||
'Перезаписати найстаріші при заповненні';
|
||||
|
||||
@override
|
||||
String get storage => 'Сховище';
|
||||
|
||||
@override
|
||||
String get dangerZone => 'Небезпечна зона';
|
||||
|
||||
@override
|
||||
String get profiles => 'Профілі';
|
||||
|
||||
@override
|
||||
String get favourites => 'Обране';
|
||||
|
||||
@override
|
||||
String get sensors => 'Датчики';
|
||||
|
||||
@override
|
||||
String get others => 'Інші';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'Модуль GPS';
|
||||
|
||||
@override
|
||||
String get liveTraffic => 'Живий трафік';
|
||||
|
||||
@override
|
||||
String get repeatersMap => 'Карта повторювачів';
|
||||
|
||||
@override
|
||||
String get spectrumScan => 'Сканування спектру';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'Журнали BLE-пакетів';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => 'База трасувань';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => 'Розмір шляху в байтах';
|
||||
|
||||
@override
|
||||
String get messageNotifications => 'Сповіщення про повідомлення';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR-сповіщення';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => 'Сповіщення про виявлення';
|
||||
|
||||
@override
|
||||
String get updateNotifications => 'Сповіщення про оновлення';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => 'Без звуку при відкритому додатку';
|
||||
|
||||
@override
|
||||
String get disableContacts => 'Вимкнути контакти';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => 'Увімкнути вкладку Датчики';
|
||||
|
||||
@override
|
||||
String get enableProfiles => 'Увімкнути профілі';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => 'Автоматична ротація маршруту';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => 'Найближчий повторювач як резерв';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory =>
|
||||
'Видалити всю історію повідомлень';
|
||||
|
||||
@override
|
||||
String get messageFontSize => 'Розмір шрифту повідомлень';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => 'Обертати карту за напрямком';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => 'Показати налагоджувальну інфо карти';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => 'Відкрити карту на повний екран';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => 'Показати SAR-маркери';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap =>
|
||||
'Показувати SAR-маркери на головній карті';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => 'Показати всі сліди контактів';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => 'Сховати повторювачі на карті';
|
||||
|
||||
@override
|
||||
String get setMapScale => 'Встановити масштаб карти';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => 'Користувацький масштаб карти збережено';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => 'Бітрейт голосу';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => 'Компресор голосу';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels =>
|
||||
'Вирівнює тихий і гучний рівні мови';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => 'Лімітер голосу';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding =>
|
||||
'Запобігає обрізанню піків перед кодуванням';
|
||||
|
||||
@override
|
||||
String get micAutoGain => 'Автопідсилення мікрофона';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel =>
|
||||
'Дозволяє рекордеру налаштувати рівень входу';
|
||||
|
||||
@override
|
||||
String get echoCancellation => 'Придушення ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => 'Придушення шуму';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages =>
|
||||
'Обрізати тишу в голосових повідомленнях';
|
||||
|
||||
@override
|
||||
String get compressor => 'Компресор';
|
||||
|
||||
@override
|
||||
String get limiter => 'Лімітер';
|
||||
|
||||
@override
|
||||
String get autoGain => 'Автопідсилення';
|
||||
|
||||
@override
|
||||
String get echoCancel => 'Ехо';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => 'Шум';
|
||||
|
||||
@override
|
||||
String get silenceTrim => 'Тиша';
|
||||
|
||||
@override
|
||||
String get maxImageSize => 'Максимальний розмір зображення';
|
||||
|
||||
@override
|
||||
String get imageCompression => 'Стиснення зображення';
|
||||
|
||||
@override
|
||||
String get grayscale => 'Відтінки сірого';
|
||||
|
||||
@override
|
||||
String get ultraMode => 'Ультра режим';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => 'Швидкі приватні GPS-оновлення';
|
||||
|
||||
@override
|
||||
String get movementThreshold => 'Поріг руху';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => 'Поріг руху швидкого GPS';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval =>
|
||||
'Інтервал активного використання швидкого GPS';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval =>
|
||||
'Інтервал оновлення при активному використанні';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => 'Повторювати близький трафік';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh =>
|
||||
'Ретранслювати через повторювачі по мережі';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding =>
|
||||
'Лише поблизу, без flood повторювачів';
|
||||
|
||||
@override
|
||||
String get multihop => 'Багатохоповий';
|
||||
|
||||
@override
|
||||
String get createProfile => 'Створити профіль';
|
||||
|
||||
@override
|
||||
String get renameProfile => 'Перейменувати профіль';
|
||||
|
||||
@override
|
||||
String get newProfile => 'Новий профіль';
|
||||
|
||||
@override
|
||||
String get manageProfiles => 'Керувати профілями';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem =>
|
||||
'Увімкніть профілі, щоб почати керувати ними.';
|
||||
|
||||
@override
|
||||
String get openMessage => 'Відкрити повідомлення';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage =>
|
||||
'Перейти до пов\'язаного SAR-повідомлення';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => 'Видалити SAR-маркер';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker =>
|
||||
'Виберіть призначення для надсилання SAR-маркера';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel =>
|
||||
'SAR-маркер надіслано на публічний канал';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR-маркер надіслано в кімнату';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => 'Завантажити з галереї';
|
||||
|
||||
@override
|
||||
String get replaceImage => 'Замінити зображення';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => 'Вибрати з галереї';
|
||||
|
||||
@override
|
||||
String get team => 'Команда';
|
||||
|
||||
@override
|
||||
String get found => 'Знайдено';
|
||||
|
||||
@override
|
||||
String get staging => 'Місце збору';
|
||||
|
||||
@override
|
||||
String get object => 'Об\'єкт';
|
||||
|
||||
@override
|
||||
String get quiet => 'Тихо';
|
||||
|
||||
@override
|
||||
String get moderate => 'Помірно';
|
||||
|
||||
@override
|
||||
String get busy => 'Зайнято';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies =>
|
||||
'Сканування спектру не знайшло частот-кандидатів';
|
||||
|
||||
@override
|
||||
String get searchMessages => 'Пошук повідомлень';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => 'Надіслати зображення з галереї';
|
||||
|
||||
@override
|
||||
String get takePhoto => 'Зробити фото';
|
||||
|
||||
@override
|
||||
String get dmOnly => 'Лише особисті повідомлення';
|
||||
|
||||
@override
|
||||
String get allMessages => 'Усі повідомлення';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => 'Надіслати в публічний канал?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination =>
|
||||
'Виберіть тип маркера та призначення';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => 'Немає доступних призначень';
|
||||
|
||||
@override
|
||||
String get image => 'Зображення';
|
||||
|
||||
@override
|
||||
String get format => 'Формат';
|
||||
|
||||
@override
|
||||
String get dimensions => 'Розміри';
|
||||
|
||||
@override
|
||||
String get segments => 'Сегменти';
|
||||
|
||||
@override
|
||||
String get transfers => 'Передачі';
|
||||
|
||||
@override
|
||||
String get downloadedBy => 'Завантажено';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => 'Зберегти налаштування виявлення';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => 'Зберегти публічну інформацію';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => 'Зберегти налаштування радіо';
|
||||
|
||||
@override
|
||||
String get savePath => 'Зберегти шлях';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => 'Стерти дані пристрою';
|
||||
|
||||
@override
|
||||
String get wipeDevice => 'Стерти пристрій';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => 'Деструктивні дії пристрою.';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings =>
|
||||
'Оберіть пресет або налаштуйте радіо вручну.';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares =>
|
||||
'Оберіть ім\'я та місцеположення, якими ділиться пристрій.';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice =>
|
||||
'Доступний простір на цьому пристрої.';
|
||||
|
||||
@override
|
||||
String get used => 'Використано';
|
||||
|
||||
@override
|
||||
String get total => 'Загалом';
|
||||
|
||||
@override
|
||||
String get renameValue => 'Перейменувати значення';
|
||||
|
||||
@override
|
||||
String get customizeFields => 'Налаштувати поля';
|
||||
|
||||
@override
|
||||
String get livePreview => 'Попередній перегляд наживо';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => 'Розклад оновлення';
|
||||
|
||||
@override
|
||||
String get noResponse => 'Немає відповіді';
|
||||
|
||||
@override
|
||||
String get refreshing => 'Оновлення';
|
||||
|
||||
@override
|
||||
String get unavailable => 'Недоступно';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors =>
|
||||
'Оберіть реле або вузол для спостереження.';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => 'Публічний ключ';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => 'Вже в контактах';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts =>
|
||||
'Підключіться до пристрою перед додаванням контактів';
|
||||
|
||||
@override
|
||||
String get fromContacts => 'З контактів';
|
||||
|
||||
@override
|
||||
String get onlineOnly => 'Лише онлайн';
|
||||
|
||||
@override
|
||||
String get inBoth => 'В обох';
|
||||
|
||||
@override
|
||||
String get source => 'Джерело';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => 'Ручне редагування маршруту';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => 'Спостережуваний mesh-маршрут';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => 'Усі повідомлення очищено';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => 'Базу трасувань очищено';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => 'Журнали пакетів очищено';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => 'Hex-дані скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => 'Режим розробника увімкнено';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => 'Режим розробника вимкнено';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => 'Буфер обміну порожній';
|
||||
|
||||
@override
|
||||
String get contactImported => 'Контакт імпортовано';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard =>
|
||||
'Посилання на контакт скопійовано в буфер';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => 'Не вдалося експортувати контакт';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => 'Немає журналів для експорту';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => 'Експортувати як CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => 'Експортувати як текст';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => 'Отримано (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => 'Час збірки';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => 'URL завантаження недоступний';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => 'Не вдається відкрити URL завантаження';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid =>
|
||||
'Перевірка оновлень доступна лише на Android';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion =>
|
||||
'Ви використовуєте останню версію';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound =>
|
||||
'Оновлення доступне, але URL завантаження не знайдено';
|
||||
|
||||
@override
|
||||
String get startTictactoe => 'Почати Tic-Tac-Toe';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => 'Tic-Tac-Toe недоступно';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => 'Tic-Tac-Toe: суперник невідомий';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => 'Tic-Tac-Toe: очікування початку';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => 'Приймає посилання для обміну';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => 'Підтримує raw hex';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => 'Зручно для буфера';
|
||||
|
||||
@override
|
||||
String get captured => 'Захоплено';
|
||||
|
||||
@override
|
||||
String get size => 'Розмір';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear =>
|
||||
'Немає користувацьких каналів для очищення.';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear =>
|
||||
'Немає контактів пристрою для очищення.';
|
||||
}
|
||||
|
||||
@@ -1656,4 +1656,916 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
|
||||
@override
|
||||
String get requestingVoice => '正在请求语音';
|
||||
|
||||
@override
|
||||
String get device => '设备';
|
||||
|
||||
@override
|
||||
String get change => '更改';
|
||||
|
||||
@override
|
||||
String get wizardOverviewDescription =>
|
||||
'此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature1 => '在主消息标签页中发送私聊消息、房间消息和频道消息。';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature2 => '通过 mesh 网络共享 SAR 标记、地图绘图、语音片段和图像。';
|
||||
|
||||
@override
|
||||
String get wizardOverviewFeature3 => '通过 BLE 或 TCP 连接,然后直接在应用中管理配套电台。';
|
||||
|
||||
@override
|
||||
String get wizardMessagingTitle => '消息与现场报告';
|
||||
|
||||
@override
|
||||
String get wizardMessagingDescription => '这里的消息不只是纯文本。应用已经支持多种操作载荷和传输流程。';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature1 => '通过一个编辑器发送私聊消息、房间消息和频道流量。';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature2 => '为常见现场报告创建 SAR 更新和可复用的 SAR 模板。';
|
||||
|
||||
@override
|
||||
String get wizardMessagingFeature3 => '传输语音会话和图像,并在界面中显示进度和空口时间估算。';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceTitle => '连接设备';
|
||||
|
||||
@override
|
||||
String get wizardConnectDeviceDescription =>
|
||||
'连接你的 MeshCore 电台,选择名称,并在继续前应用无线电预设。';
|
||||
|
||||
@override
|
||||
String get wizardSetupBadge => '设置';
|
||||
|
||||
@override
|
||||
String get wizardOverviewBadge => '概览';
|
||||
|
||||
@override
|
||||
String wizardConnectedToDevice(String deviceName) {
|
||||
return '已连接到 $deviceName';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNoDeviceConnected => '尚未连接设备';
|
||||
|
||||
@override
|
||||
String get wizardSkipForNow => '暂时跳过';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameLabel => '设备名称';
|
||||
|
||||
@override
|
||||
String get wizardDeviceNameHelp => '此名称会广播给其他 MeshCore 用户。';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionLabel => '配置区域';
|
||||
|
||||
@override
|
||||
String get wizardConfigRegionHelp =>
|
||||
'使用完整的官方 MeshCore 预设列表。默认值为 EU/UK (Narrow)。';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote1 => '请确认所选预设符合你所在地区的无线电规定。';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote2 => '该列表与 MeshCore 配置工具的官方预设源保持一致。';
|
||||
|
||||
@override
|
||||
String get wizardPresetNote3 => '在引导设置中默认保持选中 EU/UK (Narrow)。';
|
||||
|
||||
@override
|
||||
String get wizardSaving => '正在保存...';
|
||||
|
||||
@override
|
||||
String get wizardSaveAndContinue => '保存并继续';
|
||||
|
||||
@override
|
||||
String get wizardEnterDeviceName => '继续前请输入设备名称。';
|
||||
|
||||
@override
|
||||
String wizardDeviceSetupSaved(String deviceName, String presetName) {
|
||||
return '已保存 $deviceName,配置为 $presetName。';
|
||||
}
|
||||
|
||||
@override
|
||||
String get wizardNetworkTitle => '联系人、房间和中继器';
|
||||
|
||||
@override
|
||||
String get wizardNetworkDescription => '联系人标签页会整理你发现的网络以及随着时间学习到的路由。';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature1 => '在一个列表中查看团队成员、中继器、房间、频道和待处理广播。';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature2 => '当连接变得混乱时,使用智能 ping、房间登录、已学习路径和路由重置工具。';
|
||||
|
||||
@override
|
||||
String get wizardNetworkFeature3 => '无需离开应用即可创建频道并管理网络目的地。';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsTitle => '地图、轨迹与共享几何';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsDescription => '应用地图直接与消息、跟踪和 SAR 叠加层关联,而不是单独的查看器。';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature1 => '在地图上跟踪你自己的位置、队友位置和移动轨迹。';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature2 => '打开消息中的绘图,内联预览,并在需要时将其从地图上移除。';
|
||||
|
||||
@override
|
||||
String get wizardMapOpsFeature3 => '使用中继器地图视图和共享叠加层来了解现场网络覆盖范围。';
|
||||
|
||||
@override
|
||||
String get wizardToolsTitle => '消息之外的工具';
|
||||
|
||||
@override
|
||||
String get wizardToolsDescription => '这里不只有四个主标签页。应用还包含配置、诊断和可选的传感器工作流。';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature1 => '打开设备配置以更改无线电设置、遥测、TX 功率和配套设备详情。';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature2 => '当你需要受监控的传感器面板和快速刷新操作时,启用传感器标签页。';
|
||||
|
||||
@override
|
||||
String get wizardToolsFeature3 => '在排查 mesh 网络问题时使用数据包日志、频谱扫描和开发者诊断。';
|
||||
|
||||
@override
|
||||
String get contactInSensors => '在传感器中';
|
||||
|
||||
@override
|
||||
String get contactAddToSensors => '添加到传感器';
|
||||
|
||||
@override
|
||||
String get contactSetPath => '设置路径';
|
||||
|
||||
@override
|
||||
String contactAddedToSensors(String contactName) {
|
||||
return '已将 $contactName 添加到传感器';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToClearRoute(String error) {
|
||||
return '清除路由失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get contactRouteCleared => '路由已清除';
|
||||
|
||||
@override
|
||||
String contactRouteSet(String route) {
|
||||
return '路由已设置:$route';
|
||||
}
|
||||
|
||||
@override
|
||||
String contactFailedToSetRoute(String error) {
|
||||
return '设置路由失败:$error';
|
||||
}
|
||||
|
||||
@override
|
||||
String get rssi => 'RSSI';
|
||||
|
||||
@override
|
||||
String get snr => 'SNR';
|
||||
|
||||
@override
|
||||
String get ackTimeout => 'ACK 超时';
|
||||
|
||||
@override
|
||||
String get opcode => '操作码';
|
||||
|
||||
@override
|
||||
String get payload => '有效载荷';
|
||||
|
||||
@override
|
||||
String get hops => '跳数';
|
||||
|
||||
@override
|
||||
String get hashSize => '哈希大小';
|
||||
|
||||
@override
|
||||
String get pathBytes => '路径字节';
|
||||
|
||||
@override
|
||||
String get selectedPath => '选定路径';
|
||||
|
||||
@override
|
||||
String get estimatedTx => '预计传输';
|
||||
|
||||
@override
|
||||
String get senderToReceipt => '发送到接收';
|
||||
|
||||
@override
|
||||
String get receivedCopies => '收到的副本';
|
||||
|
||||
@override
|
||||
String get retryCause => '重试原因';
|
||||
|
||||
@override
|
||||
String get retryMode => '重试模式';
|
||||
|
||||
@override
|
||||
String get retryResult => '重试结果';
|
||||
|
||||
@override
|
||||
String get lastRetry => '最后重试';
|
||||
|
||||
@override
|
||||
String get rxPackets => 'RX 数据包';
|
||||
|
||||
@override
|
||||
String get mesh => 'Mesh';
|
||||
|
||||
@override
|
||||
String get rate => '速率';
|
||||
|
||||
@override
|
||||
String get window => '窗口';
|
||||
|
||||
@override
|
||||
String get posttxDelay => '发送后延迟';
|
||||
|
||||
@override
|
||||
String get bandpass => '带通';
|
||||
|
||||
@override
|
||||
String get bandpassFilterVoice => '带通语音滤波';
|
||||
|
||||
@override
|
||||
String get frequency => '频率';
|
||||
|
||||
@override
|
||||
String get australia => '澳大利亚';
|
||||
|
||||
@override
|
||||
String get australiaNarrow => '澳大利亚(窄带)';
|
||||
|
||||
@override
|
||||
String get australiaQld => '澳大利亚:QLD';
|
||||
|
||||
@override
|
||||
String get australiaSaWa => '澳大利亚:SA, WA';
|
||||
|
||||
@override
|
||||
String get newZealand => '新西兰';
|
||||
|
||||
@override
|
||||
String get newZealandNarrow => '新西兰(窄带)';
|
||||
|
||||
@override
|
||||
String get switzerland => '瑞士';
|
||||
|
||||
@override
|
||||
String get portugal433 => '葡萄牙 433';
|
||||
|
||||
@override
|
||||
String get portugal868 => '葡萄牙 868';
|
||||
|
||||
@override
|
||||
String get czechRepublicNarrow => '捷克(窄带)';
|
||||
|
||||
@override
|
||||
String get eu433mhzLongRange => '欧盟 433MHz(远距离)';
|
||||
|
||||
@override
|
||||
String get euukDeprecated => '欧盟/英国(已弃用)';
|
||||
|
||||
@override
|
||||
String get euukNarrow => '欧盟/英国(窄带)';
|
||||
|
||||
@override
|
||||
String get usacanadaRecommended => '美国/加拿大(推荐)';
|
||||
|
||||
@override
|
||||
String get vietnamDeprecated => '越南(已弃用)';
|
||||
|
||||
@override
|
||||
String get vietnamNarrow => '越南(窄带)';
|
||||
|
||||
@override
|
||||
String get active => '活动';
|
||||
|
||||
@override
|
||||
String get addContact => '添加联系人';
|
||||
|
||||
@override
|
||||
String get all => '全部';
|
||||
|
||||
@override
|
||||
String get autoResolve => '自动解析';
|
||||
|
||||
@override
|
||||
String get clearAllLabel => '清除全部';
|
||||
|
||||
@override
|
||||
String get clearRelays => '清除中继';
|
||||
|
||||
@override
|
||||
String get clearFilters => '清除过滤器';
|
||||
|
||||
@override
|
||||
String get clearRoute => '清除路由';
|
||||
|
||||
@override
|
||||
String get clearMessages => '清除消息';
|
||||
|
||||
@override
|
||||
String get clearScale => '清除比例';
|
||||
|
||||
@override
|
||||
String get clearDiscoveries => '清除发现';
|
||||
|
||||
@override
|
||||
String get clearOnlineTraceDatabase => '清除在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get clearAllChannels => '清除所有频道';
|
||||
|
||||
@override
|
||||
String get clearAllContacts => '清除所有联系人';
|
||||
|
||||
@override
|
||||
String get clearChannels => '清除频道';
|
||||
|
||||
@override
|
||||
String get clearContacts => '清除联系人';
|
||||
|
||||
@override
|
||||
String get clearPathOnMaxRetry => '最大重试时清除路径';
|
||||
|
||||
@override
|
||||
String get create => '创建';
|
||||
|
||||
@override
|
||||
String get custom => '自定义';
|
||||
|
||||
@override
|
||||
String get defaultValue => '默认';
|
||||
|
||||
@override
|
||||
String get duplicate => '复制';
|
||||
|
||||
@override
|
||||
String get editName => '编辑名称';
|
||||
|
||||
@override
|
||||
String get open => '打开';
|
||||
|
||||
@override
|
||||
String get paste => '粘贴';
|
||||
|
||||
@override
|
||||
String get preview => '预览';
|
||||
|
||||
@override
|
||||
String get remove => '移除';
|
||||
|
||||
@override
|
||||
String get rename => '重命名';
|
||||
|
||||
@override
|
||||
String get resolveAll => '解析全部';
|
||||
|
||||
@override
|
||||
String get send => '发送';
|
||||
|
||||
@override
|
||||
String get sendAnyway => '仍然发送';
|
||||
|
||||
@override
|
||||
String get share => '分享';
|
||||
|
||||
@override
|
||||
String get shareContact => '分享联系人';
|
||||
|
||||
@override
|
||||
String get trace => '追踪';
|
||||
|
||||
@override
|
||||
String get use => '使用';
|
||||
|
||||
@override
|
||||
String get useSelectedFrequency => '使用选定频率';
|
||||
|
||||
@override
|
||||
String get discovery => '发现';
|
||||
|
||||
@override
|
||||
String get discoverRepeaters => '发现中继器';
|
||||
|
||||
@override
|
||||
String get discoverSensors => '发现传感器';
|
||||
|
||||
@override
|
||||
String get repeaterDiscoverySent => '中继器发现已发送';
|
||||
|
||||
@override
|
||||
String get sensorDiscoverySent => '传感器发现已发送';
|
||||
|
||||
@override
|
||||
String get clearedPendingDiscoveries => '已清除待处理的发现。';
|
||||
|
||||
@override
|
||||
String get autoDiscovery => '自动发现';
|
||||
|
||||
@override
|
||||
String get enableAutomaticAdding => '启用自动添加';
|
||||
|
||||
@override
|
||||
String get autoaddRepeaters => '自动添加中继器';
|
||||
|
||||
@override
|
||||
String get autoaddRoomServers => '自动添加房间服务器';
|
||||
|
||||
@override
|
||||
String get autoaddSensors => '自动添加传感器';
|
||||
|
||||
@override
|
||||
String get autoaddUsers => '自动添加用户';
|
||||
|
||||
@override
|
||||
String get overwriteOldestWhenFull => '满时覆盖最旧的';
|
||||
|
||||
@override
|
||||
String get storage => '存储';
|
||||
|
||||
@override
|
||||
String get dangerZone => '危险区域';
|
||||
|
||||
@override
|
||||
String get profiles => '配置文件';
|
||||
|
||||
@override
|
||||
String get favourites => '收藏夹';
|
||||
|
||||
@override
|
||||
String get sensors => '传感器';
|
||||
|
||||
@override
|
||||
String get others => '其他';
|
||||
|
||||
@override
|
||||
String get gpsModule => 'GPS 模块';
|
||||
|
||||
@override
|
||||
String get liveTraffic => '实时流量';
|
||||
|
||||
@override
|
||||
String get repeatersMap => '中继器地图';
|
||||
|
||||
@override
|
||||
String get spectrumScan => '频谱扫描';
|
||||
|
||||
@override
|
||||
String get blePacketLogs => 'BLE 数据包日志';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabase => '在线追踪数据库';
|
||||
|
||||
@override
|
||||
String get routePathByteSize => '路由路径字节大小';
|
||||
|
||||
@override
|
||||
String get messageNotifications => '消息通知';
|
||||
|
||||
@override
|
||||
String get sarAlerts => 'SAR 警报';
|
||||
|
||||
@override
|
||||
String get discoveryNotifications => '发现通知';
|
||||
|
||||
@override
|
||||
String get updateNotifications => '更新通知';
|
||||
|
||||
@override
|
||||
String get muteWhileAppIsOpen => '应用打开时静音';
|
||||
|
||||
@override
|
||||
String get disableContacts => '禁用联系人';
|
||||
|
||||
@override
|
||||
String get enableSensorsTab => '启用传感器标签';
|
||||
|
||||
@override
|
||||
String get enableProfiles => '启用配置文件';
|
||||
|
||||
@override
|
||||
String get autoRouteRotation => '自动路由轮换';
|
||||
|
||||
@override
|
||||
String get nearestRepeaterFallback => '最近中继器回退';
|
||||
|
||||
@override
|
||||
String get deleteAllStoredMessageHistory => '删除所有存储的消息历史';
|
||||
|
||||
@override
|
||||
String get messageFontSize => '消息字体大小';
|
||||
|
||||
@override
|
||||
String get rotateMapWithHeading => '随航向旋转地图';
|
||||
|
||||
@override
|
||||
String get showMapDebugInfo => '显示地图调试信息';
|
||||
|
||||
@override
|
||||
String get openMapInFullscreen => '全屏打开地图';
|
||||
|
||||
@override
|
||||
String get showSarMarkersLabel => '显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get displaySarMarkersOnTheMainMap => '在主地图上显示 SAR 标记';
|
||||
|
||||
@override
|
||||
String get showAllContactTrailsLabel => '显示所有联系人轨迹';
|
||||
|
||||
@override
|
||||
String get hideRepeatersOnMap => '在地图上隐藏中继器';
|
||||
|
||||
@override
|
||||
String get setMapScale => '设置地图比例';
|
||||
|
||||
@override
|
||||
String get customMapScaleSaved => '自定义地图比例已保存';
|
||||
|
||||
@override
|
||||
String get voiceBitrate => '语音比特率';
|
||||
|
||||
@override
|
||||
String get voiceCompressor => '语音压缩器';
|
||||
|
||||
@override
|
||||
String get balancesQuietAndLoudSpeechLevels => '平衡安静和响亮的语音级别';
|
||||
|
||||
@override
|
||||
String get voiceLimiter => '语音限制器';
|
||||
|
||||
@override
|
||||
String get preventsClippingPeaksBeforeEncoding => '防止编码前的削波';
|
||||
|
||||
@override
|
||||
String get micAutoGain => '麦克风自动增益';
|
||||
|
||||
@override
|
||||
String get letsTheRecorderAdjustInputLevel => '让录音器调整输入级别';
|
||||
|
||||
@override
|
||||
String get echoCancellation => '回声消除';
|
||||
|
||||
@override
|
||||
String get noiseSuppression => '噪音抑制';
|
||||
|
||||
@override
|
||||
String get trimSilenceInVoiceMessages => '修剪语音消息中的静音';
|
||||
|
||||
@override
|
||||
String get compressor => '压缩器';
|
||||
|
||||
@override
|
||||
String get limiter => '限制器';
|
||||
|
||||
@override
|
||||
String get autoGain => '自动增益';
|
||||
|
||||
@override
|
||||
String get echoCancel => '回声';
|
||||
|
||||
@override
|
||||
String get noiseSuppress => '噪声';
|
||||
|
||||
@override
|
||||
String get silenceTrim => '静音';
|
||||
|
||||
@override
|
||||
String get maxImageSize => '最大图片大小';
|
||||
|
||||
@override
|
||||
String get imageCompression => '图片压缩';
|
||||
|
||||
@override
|
||||
String get grayscale => '灰度';
|
||||
|
||||
@override
|
||||
String get ultraMode => '超级模式';
|
||||
|
||||
@override
|
||||
String get fastPrivateGpsUpdates => '快速私有 GPS 更新';
|
||||
|
||||
@override
|
||||
String get movementThreshold => '移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsMovementThreshold => '快速 GPS 移动阈值';
|
||||
|
||||
@override
|
||||
String get fastGpsActiveuseInterval => '快速 GPS 活动使用间隔';
|
||||
|
||||
@override
|
||||
String get activeuseUpdateInterval => '活动使用更新间隔';
|
||||
|
||||
@override
|
||||
String get repeatNearbyTraffic => '转发附近流量';
|
||||
|
||||
@override
|
||||
String get relayThroughRepeatersAcrossTheMesh => '通过网格中的中继器转发';
|
||||
|
||||
@override
|
||||
String get nearbyOnlyWithoutRepeaterFlooding => '仅附近,无中继器泛洪';
|
||||
|
||||
@override
|
||||
String get multihop => '多跳';
|
||||
|
||||
@override
|
||||
String get createProfile => '创建配置文件';
|
||||
|
||||
@override
|
||||
String get renameProfile => '重命名配置文件';
|
||||
|
||||
@override
|
||||
String get newProfile => '新配置文件';
|
||||
|
||||
@override
|
||||
String get manageProfiles => '管理配置文件';
|
||||
|
||||
@override
|
||||
String get enableProfilesToStartManagingThem => '启用配置文件以开始管理它们。';
|
||||
|
||||
@override
|
||||
String get openMessage => '打开消息';
|
||||
|
||||
@override
|
||||
String get jumpToTheRelatedSarMessage => '跳转到相关的 SAR 消息';
|
||||
|
||||
@override
|
||||
String get removeSarMarker => '移除 SAR 标记';
|
||||
|
||||
@override
|
||||
String get pleaseSelectADestinationToSendSarMarker => '请选择目标以发送 SAR 标记';
|
||||
|
||||
@override
|
||||
String get sarMarkerBroadcastToPublicChannel => 'SAR 标记已广播到公共频道';
|
||||
|
||||
@override
|
||||
String get sarMarkerSentToRoom => 'SAR 标记已发送到房间';
|
||||
|
||||
@override
|
||||
String get loadFromGallery => '从图库加载';
|
||||
|
||||
@override
|
||||
String get replaceImage => '替换图片';
|
||||
|
||||
@override
|
||||
String get selectFromGallery => '从图库选择';
|
||||
|
||||
@override
|
||||
String get team => '团队';
|
||||
|
||||
@override
|
||||
String get found => '已找到';
|
||||
|
||||
@override
|
||||
String get staging => '集结区';
|
||||
|
||||
@override
|
||||
String get object => '物体';
|
||||
|
||||
@override
|
||||
String get quiet => '安静';
|
||||
|
||||
@override
|
||||
String get moderate => '中等';
|
||||
|
||||
@override
|
||||
String get busy => '繁忙';
|
||||
|
||||
@override
|
||||
String get spectrumScanReturnedNoCandidateFrequencies => '频谱扫描未找到候选频率';
|
||||
|
||||
@override
|
||||
String get searchMessages => '搜索消息';
|
||||
|
||||
@override
|
||||
String get sendImageFromGallery => '从图库发送图片';
|
||||
|
||||
@override
|
||||
String get takePhoto => '拍照';
|
||||
|
||||
@override
|
||||
String get dmOnly => '仅私信';
|
||||
|
||||
@override
|
||||
String get allMessages => '所有消息';
|
||||
|
||||
@override
|
||||
String get sendToPublicChannel => '发送到公共频道?';
|
||||
|
||||
@override
|
||||
String get selectMarkerTypeAndDestination => '选择标记类型和目标';
|
||||
|
||||
@override
|
||||
String get noDestinationsAvailableLabel => '没有可用的目标';
|
||||
|
||||
@override
|
||||
String get image => '图片';
|
||||
|
||||
@override
|
||||
String get format => '格式';
|
||||
|
||||
@override
|
||||
String get dimensions => '尺寸';
|
||||
|
||||
@override
|
||||
String get segments => '分段';
|
||||
|
||||
@override
|
||||
String get transfers => '传输';
|
||||
|
||||
@override
|
||||
String get downloadedBy => '已下载';
|
||||
|
||||
@override
|
||||
String get saveDiscoverySettings => '保存发现设置';
|
||||
|
||||
@override
|
||||
String get savePublicInfo => '保存公共信息';
|
||||
|
||||
@override
|
||||
String get saveRadioSettings => '保存无线电设置';
|
||||
|
||||
@override
|
||||
String get savePath => '保存路径';
|
||||
|
||||
@override
|
||||
String get wipeDeviceData => '擦除设备数据';
|
||||
|
||||
@override
|
||||
String get wipeDevice => '擦除设备';
|
||||
|
||||
@override
|
||||
String get destructiveDeviceActions => '破坏性设备操作。';
|
||||
|
||||
@override
|
||||
String get chooseAPresetOrFinetuneCustomRadioSettings => '选择预设或微调自定义无线电设置。';
|
||||
|
||||
@override
|
||||
String get chooseTheNameAndLocationThisDeviceShares => '选择此设备共享的名称和位置。';
|
||||
|
||||
@override
|
||||
String get availableSpaceOnThisDevice => '此设备上的可用空间。';
|
||||
|
||||
@override
|
||||
String get used => '已使用';
|
||||
|
||||
@override
|
||||
String get total => '总计';
|
||||
|
||||
@override
|
||||
String get renameValue => '重命名值';
|
||||
|
||||
@override
|
||||
String get customizeFields => '自定义字段';
|
||||
|
||||
@override
|
||||
String get livePreview => '实时预览';
|
||||
|
||||
@override
|
||||
String get refreshSchedule => '刷新计划';
|
||||
|
||||
@override
|
||||
String get noResponse => '无响应';
|
||||
|
||||
@override
|
||||
String get refreshing => '刷新中';
|
||||
|
||||
@override
|
||||
String get unavailable => '不可用';
|
||||
|
||||
@override
|
||||
String get pickARelayOrNodeToWatchInSensors => '选择要在传感器中监视的中继或节点。';
|
||||
|
||||
@override
|
||||
String get publicKeyLabel => '公钥';
|
||||
|
||||
@override
|
||||
String get alreadyInContacts => '已在联系人中';
|
||||
|
||||
@override
|
||||
String get connectToADeviceBeforeAddingContacts => '添加联系人前请连接到设备';
|
||||
|
||||
@override
|
||||
String get fromContacts => '来自联系人';
|
||||
|
||||
@override
|
||||
String get onlineOnly => '仅在线';
|
||||
|
||||
@override
|
||||
String get inBoth => '两者中';
|
||||
|
||||
@override
|
||||
String get source => '来源';
|
||||
|
||||
@override
|
||||
String get manualRouteEdit => '手动路由编辑';
|
||||
|
||||
@override
|
||||
String get observedMeshRoute => '观察到的网格路由';
|
||||
|
||||
@override
|
||||
String get allMessagesCleared => '所有消息已清除';
|
||||
|
||||
@override
|
||||
String get onlineTraceDatabaseCleared => '在线追踪数据库已清除';
|
||||
|
||||
@override
|
||||
String get packetLogsCleared => '数据包日志已清除';
|
||||
|
||||
@override
|
||||
String get hexDataCopiedToClipboard => '十六进制数据已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get developerModeEnabled => '开发者模式已启用';
|
||||
|
||||
@override
|
||||
String get developerModeDisabled => '开发者模式已禁用';
|
||||
|
||||
@override
|
||||
String get clipboardIsEmpty => '剪贴板为空';
|
||||
|
||||
@override
|
||||
String get contactImported => '联系人已导入';
|
||||
|
||||
@override
|
||||
String get contactLinkCopiedToClipboard => '联系人链接已复制到剪贴板';
|
||||
|
||||
@override
|
||||
String get failedToExportContact => '导出联系人失败';
|
||||
|
||||
@override
|
||||
String get noLogsToExport => '没有日志可导出';
|
||||
|
||||
@override
|
||||
String get exportAsCsv => '导出为 CSV';
|
||||
|
||||
@override
|
||||
String get exportAsText => '导出为文本';
|
||||
|
||||
@override
|
||||
String get receivedRfc3339 => '收到 (RFC3339)';
|
||||
|
||||
@override
|
||||
String get buildTime => '构建时间';
|
||||
|
||||
@override
|
||||
String get downloadUrlNotAvailable => '下载链接不可用';
|
||||
|
||||
@override
|
||||
String get cannotOpenDownloadUrl => '无法打开下载链接';
|
||||
|
||||
@override
|
||||
String get updateCheckIsOnlyAvailableOnAndroid => '更新检查仅在 Android 上可用';
|
||||
|
||||
@override
|
||||
String get youAreRunningTheLatestVersion => '您正在使用最新版本';
|
||||
|
||||
@override
|
||||
String get updateAvailableButDownloadUrlNotFound => '有更新可用但未找到下载链接';
|
||||
|
||||
@override
|
||||
String get startTictactoe => '开始井字棋';
|
||||
|
||||
@override
|
||||
String get tictactoeUnavailable => '井字棋不可用';
|
||||
|
||||
@override
|
||||
String get tictactoeOpponentUnknown => '井字棋:对手未知';
|
||||
|
||||
@override
|
||||
String get tictactoeWaitingForStart => '井字棋:等待开始';
|
||||
|
||||
@override
|
||||
String get acceptsShareLinks => '接受分享链接';
|
||||
|
||||
@override
|
||||
String get supportsRawHex => '支持原始十六进制';
|
||||
|
||||
@override
|
||||
String get clipboardfriendly => '剪贴板友好';
|
||||
|
||||
@override
|
||||
String get captured => '已捕获';
|
||||
|
||||
@override
|
||||
String get size => '大小';
|
||||
|
||||
@override
|
||||
String get noCustomChannelsToClear => '没有自定义频道可清除。';
|
||||
|
||||
@override
|
||||
String get noDeviceContactsToClear => '没有设备联系人可清除。';
|
||||
}
|
||||
|
||||
@@ -2276,5 +2276,304 @@
|
||||
"voiceUnavailable": "Głos jest obecnie niedostępny",
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Pobieranie głosu",
|
||||
"@requestingVoice": {}
|
||||
"@requestingVoice": {},
|
||||
"device": "urządzenie",
|
||||
"change": "Zmień",
|
||||
"wizardOverviewDescription": "Ta aplikacja łączy wiadomości MeshCore, terenowe aktualizacje SAR, mapy i narzędzia urządzenia w jednym miejscu.",
|
||||
"wizardOverviewFeature1": "Wysyłaj wiadomości bezpośrednie, wpisy pokojowe i wiadomości kanałowe z głównej karty Wiadomości.",
|
||||
"wizardOverviewFeature2": "Udostępniaj znaczniki SAR, rysunki mapy, klipy głosowe i obrazy przez sieć mesh.",
|
||||
"wizardOverviewFeature3": "Połącz się przez BLE lub TCP, a następnie zarządzaj radiem towarzyszącym bezpośrednio z aplikacji.",
|
||||
"wizardMessagingTitle": "Wiadomości i raporty terenowe",
|
||||
"wizardMessagingDescription": "Wiadomości to tutaj coś więcej niż zwykły tekst. Aplikacja obsługuje już kilka typów danych operacyjnych i przepływów transferu.",
|
||||
"wizardMessagingFeature1": "Wysyłaj wiadomości bezpośrednie, wpisy pokojowe i ruch kanałowy z jednego edytora.",
|
||||
"wizardMessagingFeature2": "Twórz aktualizacje SAR i wielokrotnego użytku szablony SAR do typowych raportów terenowych.",
|
||||
"wizardMessagingFeature3": "Przesyłaj sesje głosowe i obrazy z postępem i szacowanym czasem transmisji w interfejsie.",
|
||||
"wizardConnectDeviceTitle": "Połącz urządzenie",
|
||||
"wizardConnectDeviceDescription": "Podłącz radio MeshCore, wybierz nazwę i zastosuj profil radiowy przed kontynuacją.",
|
||||
"wizardSetupBadge": "Konfiguracja",
|
||||
"wizardOverviewBadge": "Przegląd",
|
||||
"wizardConnectedToDevice": "Połączono z {deviceName}",
|
||||
"wizardNoDeviceConnected": "Brak podłączonego urządzenia",
|
||||
"wizardSkipForNow": "Pomiń na razie",
|
||||
"wizardDeviceNameLabel": "Nazwa urządzenia",
|
||||
"wizardDeviceNameHelp": "Ta nazwa jest rozgłaszana innym użytkownikom MeshCore.",
|
||||
"wizardConfigRegionLabel": "Region konfiguracji",
|
||||
"wizardConfigRegionHelp": "Używa pełnej oficjalnej listy presetów MeshCore. Domyślnie wybrane jest EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Upewnij się, że wybrany preset jest zgodny z lokalnymi przepisami radiowymi.",
|
||||
"wizardPresetNote2": "Lista odpowiada oficjalnemu źródłu presetów narzędzia konfiguracji MeshCore.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) pozostaje domyślnie wybrane podczas wdrożenia.",
|
||||
"wizardSaving": "Zapisywanie...",
|
||||
"wizardSaveAndContinue": "Zapisz i kontynuuj",
|
||||
"wizardEnterDeviceName": "Wprowadź nazwę urządzenia przed kontynuacją.",
|
||||
"wizardDeviceSetupSaved": "Zapisano {deviceName} z ustawieniem {presetName}.",
|
||||
"wizardNetworkTitle": "Kontakty, pokoje i repeatery",
|
||||
"wizardNetworkDescription": "Karta Kontakty porządkuje sieć, którą odkrywasz, oraz trasy, których uczysz się z czasem.",
|
||||
"wizardNetworkFeature1": "Przeglądaj członków zespołu, repeatery, pokoje, kanały i oczekujące ogłoszenia na jednej liście.",
|
||||
"wizardNetworkFeature2": "Używaj smart ping, logowania do pokoi, poznanych ścieżek i narzędzi resetowania tras, gdy łączność staje się chaotyczna.",
|
||||
"wizardNetworkFeature3": "Twórz kanały i zarządzaj celami sieciowymi bez opuszczania aplikacji.",
|
||||
"wizardMapOpsTitle": "Mapa, ślady i współdzielona geometria",
|
||||
"wizardMapOpsDescription": "Mapa aplikacji jest bezpośrednio powiązana z wiadomościami, śledzeniem i nakładkami SAR zamiast być osobnym podglądem.",
|
||||
"wizardMapOpsFeature1": "Śledź własną pozycję, lokalizacje członków zespołu i ślady ruchu na mapie.",
|
||||
"wizardMapOpsFeature2": "Otwieraj rysunki z wiadomości, podglądaj je w miejscu i usuwaj z mapy w razie potrzeby.",
|
||||
"wizardMapOpsFeature3": "Korzystaj z widoków map repeaterów i współdzielonych nakładek, aby zrozumieć zasięg sieci w terenie.",
|
||||
"wizardToolsTitle": "Narzędzia poza wiadomościami",
|
||||
"wizardToolsDescription": "Jest tu więcej niż cztery główne karty. Aplikacja obejmuje także konfigurację, diagnostykę i opcjonalne przepływy czujników.",
|
||||
"wizardToolsFeature1": "Otwórz konfigurację urządzenia, aby zmienić ustawienia radia, telemetrię, moc TX i szczegóły urządzenia towarzyszącego.",
|
||||
"wizardToolsFeature2": "Włącz kartę Czujniki, gdy chcesz mieć pulpity monitorowanych czujników i szybkie odświeżanie.",
|
||||
"wizardToolsFeature3": "Używaj logów pakietów, skanowania widma i diagnostyki deweloperskiej podczas rozwiązywania problemów z mesh.",
|
||||
"contactInSensors": "W Czujnikach",
|
||||
"contactAddToSensors": "Dodaj do Czujników",
|
||||
"contactSetPath": "Ustaw ścieżkę",
|
||||
"contactAddedToSensors": "{contactName} dodano do Czujników",
|
||||
"contactFailedToClearRoute": "Nie udało się wyczyścić trasy: {error}",
|
||||
"contactRouteCleared": "Trasa wyczyszczona",
|
||||
"contactRouteSet": "Ustawiono trasę: {route}",
|
||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Limit czasu ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Ładunek",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Rozmiar hash",
|
||||
"pathBytes": "Bajty ścieżki",
|
||||
"selectedPath": "Wybrana ścieżka",
|
||||
"estimatedTx": "Szacowane nadawanie",
|
||||
"senderToReceipt": "Od nadawcy do odbioru",
|
||||
"receivedCopies": "Otrzymane kopie",
|
||||
"retryCause": "Przyczyna ponowienia",
|
||||
"retryMode": "Tryb ponowienia",
|
||||
"retryResult": "Wynik ponowienia",
|
||||
"lastRetry": "Ostatnia próba",
|
||||
"rxPackets": "Pakiety RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Szybkość",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Opóźnienie po nadawaniu",
|
||||
"bandpass": "Pasmowy",
|
||||
"bandpassFilterVoice": "Filtr pasmowy głosu",
|
||||
"frequency": "Częstotliwość",
|
||||
"australia": "Australia",
|
||||
"australiaNarrow": "Australia (wąski)",
|
||||
"australiaQld": "Australia: QLD",
|
||||
"australiaSaWa": "Australia: SA, WA",
|
||||
"newZealand": "Nowa Zelandia",
|
||||
"newZealandNarrow": "Nowa Zelandia (wąski)",
|
||||
"switzerland": "Szwajcaria",
|
||||
"portugal433": "Portugalia 433",
|
||||
"portugal868": "Portugalia 868",
|
||||
"czechRepublicNarrow": "Czechy (wąski)",
|
||||
"eu433mhzLongRange": "EU 433MHz (daleki zasięg)",
|
||||
"euukDeprecated": "EU/UK (przestarzały)",
|
||||
"euukNarrow": "EU/UK (wąski)",
|
||||
"usacanadaRecommended": "USA/Kanada (zalecany)",
|
||||
"vietnamDeprecated": "Wietnam (przestarzały)",
|
||||
"vietnamNarrow": "Wietnam (wąski)",
|
||||
"active": "Aktywny",
|
||||
"addContact": "Dodaj kontakt",
|
||||
"all": "Wszystko",
|
||||
"autoResolve": "Automatyczne rozwiązanie",
|
||||
"clearAllLabel": "Wyczyść wszystko",
|
||||
"clearRelays": "Wyczyść przekaźniki",
|
||||
"clearFilters": "Wyczyść filtry",
|
||||
"clearRoute": "Wyczyść trasę",
|
||||
"clearMessages": "Wyczyść wiadomości",
|
||||
"clearScale": "Wyczyść skalę",
|
||||
"clearDiscoveries": "Wyczyść odkrycia",
|
||||
"clearOnlineTraceDatabase": "Wyczyść bazę śladów",
|
||||
"clearAllChannels": "Wyczyść wszystkie kanały",
|
||||
"clearAllContacts": "Wyczyść wszystkie kontakty",
|
||||
"clearChannels": "Wyczyść kanały",
|
||||
"clearContacts": "Wyczyść kontakty",
|
||||
"clearPathOnMaxRetry": "Wyczyść ścieżkę przy maks. próbie",
|
||||
"create": "Utwórz",
|
||||
"custom": "Niestandardowy",
|
||||
"defaultValue": "Domyślny",
|
||||
"duplicate": "Duplikuj",
|
||||
"editName": "Edytuj nazwę",
|
||||
"open": "Otwórz",
|
||||
"paste": "Wklej",
|
||||
"preview": "Podgląd",
|
||||
"remove": "Usuń",
|
||||
"rename": "Zmień nazwę",
|
||||
"resolveAll": "Rozwiąż wszystko",
|
||||
"send": "Wyślij",
|
||||
"sendAnyway": "Wyślij mimo to",
|
||||
"share": "Udostępnij",
|
||||
"shareContact": "Udostępnij kontakt",
|
||||
"trace": "Śledzenie",
|
||||
"use": "Użyj",
|
||||
"useSelectedFrequency": "Użyj wybranej częstotliwości",
|
||||
"discovery": "Wykrywanie",
|
||||
"discoverRepeaters": "Wykryj przekaźniki",
|
||||
"discoverSensors": "Wykryj czujniki",
|
||||
"repeaterDiscoverySent": "Wykrywanie przekaźników wysłane",
|
||||
"sensorDiscoverySent": "Wykrywanie czujników wysłane",
|
||||
"clearedPendingDiscoveries": "Wyczyszczono oczekujące odkrycia.",
|
||||
"autoDiscovery": "Automatyczne wykrywanie",
|
||||
"enableAutomaticAdding": "Włącz automatyczne dodawanie",
|
||||
"autoaddRepeaters": "Automatycznie dodaj przekaźniki",
|
||||
"autoaddRoomServers": "Automatycznie dodaj serwery pokoi",
|
||||
"autoaddSensors": "Automatycznie dodaj czujniki",
|
||||
"autoaddUsers": "Automatycznie dodaj użytkowników",
|
||||
"overwriteOldestWhenFull": "Nadpisz najstarsze gdy pełne",
|
||||
"storage": "Pamięć",
|
||||
"dangerZone": "Strefa zagrożenia",
|
||||
"profiles": "Profile",
|
||||
"favourites": "Ulubione",
|
||||
"sensors": "Czujniki",
|
||||
"others": "Inne",
|
||||
"gpsModule": "Moduł GPS",
|
||||
"liveTraffic": "Ruch na żywo",
|
||||
"repeatersMap": "Mapa przekaźników",
|
||||
"spectrumScan": "Skanowanie spektrum",
|
||||
"blePacketLogs": "Logi pakietów BLE",
|
||||
"onlineTraceDatabase": "Baza śladów",
|
||||
"routePathByteSize": "Rozmiar ścieżki w bajtach",
|
||||
"messageNotifications": "Powiadomienia o wiadomościach",
|
||||
"sarAlerts": "Alerty SAR",
|
||||
"discoveryNotifications": "Powiadomienia o wykrywaniu",
|
||||
"updateNotifications": "Powiadomienia o aktualizacjach",
|
||||
"muteWhileAppIsOpen": "Wycisz gdy aplikacja jest otwarta",
|
||||
"disableContacts": "Wyłącz kontakty",
|
||||
"enableSensorsTab": "Włącz kartę Czujniki",
|
||||
"enableProfiles": "Włącz profile",
|
||||
"autoRouteRotation": "Automatyczna rotacja trasy",
|
||||
"nearestRepeaterFallback": "Najbliższy przekaźnik awaryjny",
|
||||
"deleteAllStoredMessageHistory": "Usuń całą historię wiadomości",
|
||||
"messageFontSize": "Rozmiar czcionki wiadomości",
|
||||
"rotateMapWithHeading": "Obracaj mapę z kierunkiem",
|
||||
"showMapDebugInfo": "Pokaż info debugowania mapy",
|
||||
"openMapInFullscreen": "Otwórz mapę na pełnym ekranie",
|
||||
"showSarMarkersLabel": "Pokaż znaczniki SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Wyświetlaj znaczniki SAR na głównej mapie",
|
||||
"showAllContactTrailsLabel": "Pokaż wszystkie ślady kontaktów",
|
||||
"hideRepeatersOnMap": "Ukryj przekaźniki na mapie",
|
||||
"setMapScale": "Ustaw skalę mapy",
|
||||
"customMapScaleSaved": "Niestandardowa skala mapy zapisana",
|
||||
"voiceBitrate": "Bitrate głosu",
|
||||
"voiceCompressor": "Kompresor głosu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Wyrównuje ciche i głośne poziomy mowy",
|
||||
"voiceLimiter": "Limiter głosu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Zapobiega przycinaniu szczytów przed kodowaniem",
|
||||
"micAutoGain": "Automatyczne wzmocnienie mikrofonu",
|
||||
"letsTheRecorderAdjustInputLevel": "Pozwala nagrywarkowi dostosować poziom wejścia",
|
||||
"echoCancellation": "Usuwanie echa",
|
||||
"noiseSuppression": "Tłumienie szumu",
|
||||
"trimSilenceInVoiceMessages": "Przytnij ciszę w wiadomościach głosowych",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Limiter",
|
||||
"autoGain": "Automatyczne wzmocnienie",
|
||||
"echoCancel": "Echo",
|
||||
"noiseSuppress": "Szum",
|
||||
"silenceTrim": "Cisza",
|
||||
"maxImageSize": "Maksymalny rozmiar obrazu",
|
||||
"imageCompression": "Kompresja obrazu",
|
||||
"grayscale": "Skala szarości",
|
||||
"ultraMode": "Tryb ultra",
|
||||
"fastPrivateGpsUpdates": "Szybkie prywatne aktualizacje GPS",
|
||||
"movementThreshold": "Próg ruchu",
|
||||
"fastGpsMovementThreshold": "Próg ruchu szybkiego GPS",
|
||||
"fastGpsActiveuseInterval": "Interwał aktywnego użycia szybkiego GPS",
|
||||
"activeuseUpdateInterval": "Interwał aktualizacji przy aktywnym użyciu",
|
||||
"repeatNearbyTraffic": "Powtarzaj pobliski ruch",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Przekazuj przez przekaźniki w sieci",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Tylko w pobliżu, bez zalewania przekaźników",
|
||||
"multihop": "Wieloskokowy",
|
||||
"createProfile": "Utwórz profil",
|
||||
"renameProfile": "Zmień nazwę profilu",
|
||||
"newProfile": "Nowy profil",
|
||||
"manageProfiles": "Zarządzaj profilami",
|
||||
"enableProfilesToStartManagingThem": "Włącz profile, aby zacząć nimi zarządzać.",
|
||||
"openMessage": "Otwórz wiadomość",
|
||||
"jumpToTheRelatedSarMessage": "Przejdź do powiązanej wiadomości SAR",
|
||||
"removeSarMarker": "Usuń znacznik SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Wybierz cel wysłania znacznika SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Znacznik SAR wysłany na kanał publiczny",
|
||||
"sarMarkerSentToRoom": "Znacznik SAR wysłany do pokoju",
|
||||
"loadFromGallery": "Załaduj z galerii",
|
||||
"replaceImage": "Zamień obraz",
|
||||
"selectFromGallery": "Wybierz z galerii",
|
||||
"team": "Zespół",
|
||||
"found": "Znaleziono",
|
||||
"staging": "Punkt zbiórki",
|
||||
"object": "Obiekt",
|
||||
"quiet": "Cicho",
|
||||
"moderate": "Umiarkowane",
|
||||
"busy": "Zajęty",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Skanowanie spektrum nie znalazło częstotliwości kandydatów",
|
||||
"searchMessages": "Szukaj wiadomości",
|
||||
"sendImageFromGallery": "Wyślij obraz z galerii",
|
||||
"takePhoto": "Zrób zdjęcie",
|
||||
"dmOnly": "Tylko wiadomość bezpośrednia",
|
||||
"allMessages": "Wszystkie wiadomości",
|
||||
"sendToPublicChannel": "Wysłać na kanał publiczny?",
|
||||
"selectMarkerTypeAndDestination": "Wybierz typ znacznika i cel",
|
||||
"noDestinationsAvailableLabel": "Brak dostępnych celów",
|
||||
"image": "Obraz",
|
||||
"format": "Format",
|
||||
"dimensions": "Wymiary",
|
||||
"segments": "Segmenty",
|
||||
"transfers": "Transfery",
|
||||
"downloadedBy": "Pobrane przez",
|
||||
"saveDiscoverySettings": "Zapisz ustawienia wykrywania",
|
||||
"savePublicInfo": "Zapisz informacje publiczne",
|
||||
"saveRadioSettings": "Zapisz ustawienia radia",
|
||||
"savePath": "Zapisz ścieżkę",
|
||||
"wipeDeviceData": "Wyczyść dane urządzenia",
|
||||
"wipeDevice": "Wyczyść urządzenie",
|
||||
"destructiveDeviceActions": "Destrukcyjne akcje na urządzeniu.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Wybierz ustawienie wstępne lub dostosuj ustawienia radia.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Wybierz nazwę i lokalizację udostępnianą przez to urządzenie.",
|
||||
"availableSpaceOnThisDevice": "Dostępne miejsce na tym urządzeniu.",
|
||||
"used": "Użyto",
|
||||
"total": "Razem",
|
||||
"renameValue": "Zmień nazwę wartości",
|
||||
"customizeFields": "Dostosuj pola",
|
||||
"livePreview": "Podgląd na żywo",
|
||||
"refreshSchedule": "Harmonogram odświeżania",
|
||||
"noResponse": "Brak odpowiedzi",
|
||||
"refreshing": "Odświeżanie",
|
||||
"unavailable": "Niedostępny",
|
||||
"pickARelayOrNodeToWatchInSensors": "Wybierz przekaźnik lub węzeł do obserwacji.",
|
||||
"publicKeyLabel": "Klucz publiczny",
|
||||
"alreadyInContacts": "Już w kontaktach",
|
||||
"connectToADeviceBeforeAddingContacts": "Połącz się z urządzeniem przed dodaniem kontaktów",
|
||||
"fromContacts": "Z kontaktów",
|
||||
"onlineOnly": "Tylko online",
|
||||
"inBoth": "W obu",
|
||||
"source": "Źródło",
|
||||
"manualRouteEdit": "Ręczna edycja trasy",
|
||||
"observedMeshRoute": "Obserwowana trasa sieci",
|
||||
"allMessagesCleared": "Wszystkie wiadomości wyczyszczone",
|
||||
"onlineTraceDatabaseCleared": "Baza śladów wyczyszczona",
|
||||
"packetLogsCleared": "Logi pakietów wyczyszczone",
|
||||
"hexDataCopiedToClipboard": "Dane hex skopiowane do schowka",
|
||||
"developerModeEnabled": "Tryb programisty włączony",
|
||||
"developerModeDisabled": "Tryb programisty wyłączony",
|
||||
"clipboardIsEmpty": "Schowek jest pusty",
|
||||
"contactImported": "Kontakt zaimportowany",
|
||||
"contactLinkCopiedToClipboard": "Link kontaktu skopiowany do schowka",
|
||||
"failedToExportContact": "Nie udało się wyeksportować kontaktu",
|
||||
"noLogsToExport": "Brak logów do eksportu",
|
||||
"exportAsCsv": "Eksportuj jako CSV",
|
||||
"exportAsText": "Eksportuj jako tekst",
|
||||
"receivedRfc3339": "Odebrano (RFC3339)",
|
||||
"buildTime": "Czas budowania",
|
||||
"downloadUrlNotAvailable": "URL pobierania niedostępny",
|
||||
"cannotOpenDownloadUrl": "Nie można otworzyć URL pobierania",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Sprawdzanie aktualizacji dostępne tylko na Androidzie",
|
||||
"youAreRunningTheLatestVersion": "Używasz najnowszej wersji",
|
||||
"updateAvailableButDownloadUrlNotFound": "Aktualizacja dostępna, ale URL pobierania nie znaleziony",
|
||||
"startTictactoe": "Rozpocznij Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe niedostępny",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: przeciwnik nieznany",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: oczekiwanie na start",
|
||||
"acceptsShareLinks": "Akceptuje udostępnione linki",
|
||||
"supportsRawHex": "Obsługuje surowy hex",
|
||||
"clipboardfriendly": "Przyjazne dla schowka",
|
||||
"captured": "Przechwycone",
|
||||
"size": "Rozmiar",
|
||||
"noCustomChannelsToClear": "Brak niestandardowych kanałów do wyczyszczenia.",
|
||||
"noDeviceContactsToClear": "Brak kontaktów urządzenia do wyczyszczenia."
|
||||
}
|
||||
|
||||
@@ -2276,5 +2276,304 @@
|
||||
"voiceUnavailable": "A voz não está disponível no momento",
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Solicitando voz",
|
||||
"@requestingVoice": {}
|
||||
"@requestingVoice": {},
|
||||
"device": "dispositivo",
|
||||
"change": "Alterar",
|
||||
"wizardOverviewDescription": "Este aplicativo reúne mensagens MeshCore, atualizações SAR em campo, mapas e ferramentas do dispositivo em um só lugar.",
|
||||
"wizardOverviewFeature1": "Envie mensagens diretas, publicações em salas e mensagens de canal pela guia principal Mensagens.",
|
||||
"wizardOverviewFeature2": "Compartilhe marcadores SAR, desenhos de mapa, clipes de voz e imagens pela malha.",
|
||||
"wizardOverviewFeature3": "Conecte-se via BLE ou TCP e depois gerencie o rádio complementar dentro do aplicativo.",
|
||||
"wizardMessagingTitle": "Mensagens e relatórios de campo",
|
||||
"wizardMessagingDescription": "Aqui, as mensagens são mais do que texto simples. O aplicativo já oferece suporte a várias cargas operacionais e fluxos de transferência.",
|
||||
"wizardMessagingFeature1": "Envie mensagens diretas, publicações em salas e tráfego de canal a partir de um único compositor.",
|
||||
"wizardMessagingFeature2": "Crie atualizações SAR e modelos SAR reutilizáveis para relatórios de campo comuns.",
|
||||
"wizardMessagingFeature3": "Transfira sessões de voz e imagens com progresso e estimativas de tempo de transmissão na interface.",
|
||||
"wizardConnectDeviceTitle": "Conectar dispositivo",
|
||||
"wizardConnectDeviceDescription": "Conecte seu rádio MeshCore, escolha um nome e aplique uma predefinição de rádio antes de continuar.",
|
||||
"wizardSetupBadge": "Configuração",
|
||||
"wizardOverviewBadge": "Visão geral",
|
||||
"wizardConnectedToDevice": "Conectado a {deviceName}",
|
||||
"wizardNoDeviceConnected": "Nenhum dispositivo conectado ainda",
|
||||
"wizardSkipForNow": "Ignorar por enquanto",
|
||||
"wizardDeviceNameLabel": "Nome do dispositivo",
|
||||
"wizardDeviceNameHelp": "Esse nome é anunciado para outros usuários do MeshCore.",
|
||||
"wizardConfigRegionLabel": "Região de configuração",
|
||||
"wizardConfigRegionHelp": "Usa a lista oficial completa de predefinições do MeshCore. O padrão é EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Certifique-se de que a predefinição selecionada corresponda às regulamentações locais de rádio.",
|
||||
"wizardPresetNote2": "A lista corresponde ao feed oficial de predefinições da ferramenta de configuração do MeshCore.",
|
||||
"wizardPresetNote3": "EU/UK (Narrow) permanece selecionado por padrão durante a configuração inicial.",
|
||||
"wizardSaving": "Salvando...",
|
||||
"wizardSaveAndContinue": "Salvar e continuar",
|
||||
"wizardEnterDeviceName": "Digite um nome para o dispositivo antes de continuar.",
|
||||
"wizardDeviceSetupSaved": "{deviceName} salvo com {presetName}.",
|
||||
"wizardNetworkTitle": "Contatos, salas e repetidores",
|
||||
"wizardNetworkDescription": "A guia Contatos organiza a rede que você descobre e as rotas aprendidas ao longo do tempo.",
|
||||
"wizardNetworkFeature1": "Revise membros da equipe, repetidores, salas, canais e anúncios pendentes em uma única lista.",
|
||||
"wizardNetworkFeature2": "Use ping inteligente, login em salas, caminhos aprendidos e ferramentas de redefinição de rota quando a conectividade ficar confusa.",
|
||||
"wizardNetworkFeature3": "Crie canais e gerencie destinos de rede sem sair do aplicativo.",
|
||||
"wizardMapOpsTitle": "Mapa, trilhas e geometria compartilhada",
|
||||
"wizardMapOpsDescription": "O mapa do aplicativo está ligado diretamente às mensagens, ao rastreamento e às sobreposições SAR, em vez de ser um visualizador separado.",
|
||||
"wizardMapOpsFeature1": "Acompanhe sua própria posição, as localizações da equipe e as trilhas de movimento no mapa.",
|
||||
"wizardMapOpsFeature2": "Abra desenhos de mensagens, visualize-os em linha e remova-os do mapa quando necessário.",
|
||||
"wizardMapOpsFeature3": "Use vistas de mapa de repetidores e sobreposições compartilhadas para entender o alcance da rede em campo.",
|
||||
"wizardToolsTitle": "Ferramentas além das mensagens",
|
||||
"wizardToolsDescription": "Há mais aqui do que as quatro guias principais. O aplicativo também inclui configuração, diagnóstico e fluxos opcionais de sensores.",
|
||||
"wizardToolsFeature1": "Abra a configuração do dispositivo para alterar ajustes de rádio, telemetria, potência TX e detalhes do equipamento complementar.",
|
||||
"wizardToolsFeature2": "Ative a guia Sensores quando quiser painéis monitorados e ações de atualização rápida.",
|
||||
"wizardToolsFeature3": "Use logs de pacotes, varredura de espectro e diagnóstico de desenvolvedor ao solucionar problemas da malha.",
|
||||
"contactInSensors": "Nos Sensores",
|
||||
"contactAddToSensors": "Adicionar aos Sensores",
|
||||
"contactSetPath": "Definir rota",
|
||||
"contactAddedToSensors": "{contactName} adicionado a Sensores",
|
||||
"contactFailedToClearRoute": "Falha ao limpar a rota: {error}",
|
||||
"contactRouteCleared": "Rota limpa",
|
||||
"contactRouteSet": "Rota definida: {route}",
|
||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Tempo limite ACK",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Carga útil",
|
||||
"hops": "Saltos",
|
||||
"hashSize": "Tamanho do hash",
|
||||
"pathBytes": "Bytes do caminho",
|
||||
"selectedPath": "Caminho selecionado",
|
||||
"estimatedTx": "Transmissão estimada",
|
||||
"senderToReceipt": "Do remetente ao recibo",
|
||||
"receivedCopies": "Cópias recebidas",
|
||||
"retryCause": "Causa da nova tentativa",
|
||||
"retryMode": "Modo de nova tentativa",
|
||||
"retryResult": "Resultado da nova tentativa",
|
||||
"lastRetry": "Última tentativa",
|
||||
"rxPackets": "Pacotes RX",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Taxa",
|
||||
"window": "Janela",
|
||||
"posttxDelay": "Atraso pós-tx",
|
||||
"bandpass": "Passa-banda",
|
||||
"bandpassFilterVoice": "Filtro passa-banda de voz",
|
||||
"frequency": "Frequência",
|
||||
"australia": "Austrália",
|
||||
"australiaNarrow": "Austrália (Estreito)",
|
||||
"australiaQld": "Austrália: QLD",
|
||||
"australiaSaWa": "Austrália: SA, WA",
|
||||
"newZealand": "Nova Zelândia",
|
||||
"newZealandNarrow": "Nova Zelândia (Estreito)",
|
||||
"switzerland": "Suíça",
|
||||
"portugal433": "Portugal 433",
|
||||
"portugal868": "Portugal 868",
|
||||
"czechRepublicNarrow": "República Checa (Estreito)",
|
||||
"eu433mhzLongRange": "UE 433MHz (Longo Alcance)",
|
||||
"euukDeprecated": "UE/RU (Obsoleto)",
|
||||
"euukNarrow": "UE/RU (Estreito)",
|
||||
"usacanadaRecommended": "EUA/Canadá (Recomendado)",
|
||||
"vietnamDeprecated": "Vietname (Obsoleto)",
|
||||
"vietnamNarrow": "Vietname (Estreito)",
|
||||
"active": "Ativo",
|
||||
"addContact": "Adicionar contacto",
|
||||
"all": "Tudo",
|
||||
"autoResolve": "Resolver automaticamente",
|
||||
"clearAllLabel": "Limpar tudo",
|
||||
"clearRelays": "Limpar relays",
|
||||
"clearFilters": "Limpar filtros",
|
||||
"clearRoute": "Limpar rota",
|
||||
"clearMessages": "Limpar mensagens",
|
||||
"clearScale": "Limpar escala",
|
||||
"clearDiscoveries": "Limpar descobertas",
|
||||
"clearOnlineTraceDatabase": "Limpar base de dados de rastreio",
|
||||
"clearAllChannels": "Limpar todos os canais",
|
||||
"clearAllContacts": "Limpar todos os contactos",
|
||||
"clearChannels": "Limpar canais",
|
||||
"clearContacts": "Limpar contactos",
|
||||
"clearPathOnMaxRetry": "Limpar caminho no máx. de tentativas",
|
||||
"create": "Criar",
|
||||
"custom": "Personalizado",
|
||||
"defaultValue": "Padrão",
|
||||
"duplicate": "Duplicar",
|
||||
"editName": "Editar nome",
|
||||
"open": "Abrir",
|
||||
"paste": "Colar",
|
||||
"preview": "Pré-visualização",
|
||||
"remove": "Remover",
|
||||
"rename": "Renomear",
|
||||
"resolveAll": "Resolver tudo",
|
||||
"send": "Enviar",
|
||||
"sendAnyway": "Enviar mesmo assim",
|
||||
"share": "Partilhar",
|
||||
"shareContact": "Partilhar contacto",
|
||||
"trace": "Rastreio",
|
||||
"use": "Usar",
|
||||
"useSelectedFrequency": "Usar frequência selecionada",
|
||||
"discovery": "Descoberta",
|
||||
"discoverRepeaters": "Descobrir repetidores",
|
||||
"discoverSensors": "Descobrir sensores",
|
||||
"repeaterDiscoverySent": "Descoberta de repetidores enviada",
|
||||
"sensorDiscoverySent": "Descoberta de sensores enviada",
|
||||
"clearedPendingDiscoveries": "Descobertas pendentes limpas.",
|
||||
"autoDiscovery": "Descoberta automática",
|
||||
"enableAutomaticAdding": "Ativar adição automática",
|
||||
"autoaddRepeaters": "Adicionar repetidores automaticamente",
|
||||
"autoaddRoomServers": "Adicionar servidores de sala automaticamente",
|
||||
"autoaddSensors": "Adicionar sensores automaticamente",
|
||||
"autoaddUsers": "Adicionar utilizadores automaticamente",
|
||||
"overwriteOldestWhenFull": "Sobrescrever mais antigos quando cheio",
|
||||
"storage": "Armazenamento",
|
||||
"dangerZone": "Zona de perigo",
|
||||
"profiles": "Perfis",
|
||||
"favourites": "Favoritos",
|
||||
"sensors": "Sensores",
|
||||
"others": "Outros",
|
||||
"gpsModule": "Módulo GPS",
|
||||
"liveTraffic": "Tráfego ao vivo",
|
||||
"repeatersMap": "Mapa de repetidores",
|
||||
"spectrumScan": "Varredura de espectro",
|
||||
"blePacketLogs": "Registos de pacotes BLE",
|
||||
"onlineTraceDatabase": "Base de dados de rastreio",
|
||||
"routePathByteSize": "Tamanho do caminho em bytes",
|
||||
"messageNotifications": "Notificações de mensagens",
|
||||
"sarAlerts": "Alertas SAR",
|
||||
"discoveryNotifications": "Notificações de descoberta",
|
||||
"updateNotifications": "Notificações de atualização",
|
||||
"muteWhileAppIsOpen": "Silenciar com a app aberta",
|
||||
"disableContacts": "Desativar contactos",
|
||||
"enableSensorsTab": "Ativar separador Sensores",
|
||||
"enableProfiles": "Ativar perfis",
|
||||
"autoRouteRotation": "Rotação automática de rota",
|
||||
"nearestRepeaterFallback": "Repetidor mais próximo como reserva",
|
||||
"deleteAllStoredMessageHistory": "Apagar todo o histórico de mensagens",
|
||||
"messageFontSize": "Tamanho da fonte de mensagens",
|
||||
"rotateMapWithHeading": "Rodar mapa com direção",
|
||||
"showMapDebugInfo": "Mostrar info de depuração do mapa",
|
||||
"openMapInFullscreen": "Abrir mapa em ecrã inteiro",
|
||||
"showSarMarkersLabel": "Mostrar marcadores SAR",
|
||||
"displaySarMarkersOnTheMainMap": "Mostrar marcadores SAR no mapa principal",
|
||||
"showAllContactTrailsLabel": "Mostrar todos os rastos de contactos",
|
||||
"hideRepeatersOnMap": "Ocultar repetidores no mapa",
|
||||
"setMapScale": "Definir escala do mapa",
|
||||
"customMapScaleSaved": "Escala de mapa personalizada guardada",
|
||||
"voiceBitrate": "Taxa de bits de voz",
|
||||
"voiceCompressor": "Compressor de voz",
|
||||
"balancesQuietAndLoudSpeechLevels": "Equilibra níveis de fala suaves e altos",
|
||||
"voiceLimiter": "Limitador de voz",
|
||||
"preventsClippingPeaksBeforeEncoding": "Previne corte de picos antes da codificação",
|
||||
"micAutoGain": "Ganho automático do microfone",
|
||||
"letsTheRecorderAdjustInputLevel": "Permite ao gravador ajustar o nível de entrada",
|
||||
"echoCancellation": "Cancelamento de eco",
|
||||
"noiseSuppression": "Supressão de ruído",
|
||||
"trimSilenceInVoiceMessages": "Cortar silêncio em mensagens de voz",
|
||||
"compressor": "Compressor",
|
||||
"limiter": "Limitador",
|
||||
"autoGain": "Ganho automático",
|
||||
"echoCancel": "Eco",
|
||||
"noiseSuppress": "Ruído",
|
||||
"silenceTrim": "Silêncio",
|
||||
"maxImageSize": "Tamanho máximo de imagem",
|
||||
"imageCompression": "Compressão de imagem",
|
||||
"grayscale": "Escala de cinzentos",
|
||||
"ultraMode": "Modo ultra",
|
||||
"fastPrivateGpsUpdates": "Atualizações GPS privadas rápidas",
|
||||
"movementThreshold": "Limiar de movimento",
|
||||
"fastGpsMovementThreshold": "Limiar de movimento GPS rápido",
|
||||
"fastGpsActiveuseInterval": "Intervalo de uso ativo GPS rápido",
|
||||
"activeuseUpdateInterval": "Intervalo de atualização de uso ativo",
|
||||
"repeatNearbyTraffic": "Repetir tráfego próximo",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Retransmitir através de repetidores na rede",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Apenas próximos, sem flood de repetidores",
|
||||
"multihop": "Multi-salto",
|
||||
"createProfile": "Criar perfil",
|
||||
"renameProfile": "Renomear perfil",
|
||||
"newProfile": "Novo perfil",
|
||||
"manageProfiles": "Gerir perfis",
|
||||
"enableProfilesToStartManagingThem": "Ative os perfis para começar a geri-los.",
|
||||
"openMessage": "Abrir mensagem",
|
||||
"jumpToTheRelatedSarMessage": "Ir para a mensagem SAR relacionada",
|
||||
"removeSarMarker": "Remover marcador SAR",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Selecione um destino para enviar o marcador SAR",
|
||||
"sarMarkerBroadcastToPublicChannel": "Marcador SAR transmitido no canal público",
|
||||
"sarMarkerSentToRoom": "Marcador SAR enviado para a sala",
|
||||
"loadFromGallery": "Carregar da galeria",
|
||||
"replaceImage": "Substituir imagem",
|
||||
"selectFromGallery": "Selecionar da galeria",
|
||||
"team": "Equipa",
|
||||
"found": "Encontrado",
|
||||
"staging": "Área de reunião",
|
||||
"object": "Objeto",
|
||||
"quiet": "Silencioso",
|
||||
"moderate": "Moderado",
|
||||
"busy": "Ocupado",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "A varredura de espectro não encontrou frequências candidatas",
|
||||
"searchMessages": "Pesquisar mensagens",
|
||||
"sendImageFromGallery": "Enviar imagem da galeria",
|
||||
"takePhoto": "Tirar foto",
|
||||
"dmOnly": "Apenas mensagem direta",
|
||||
"allMessages": "Todas as mensagens",
|
||||
"sendToPublicChannel": "Enviar para o canal público?",
|
||||
"selectMarkerTypeAndDestination": "Selecione tipo de marcador e destino",
|
||||
"noDestinationsAvailableLabel": "Nenhum destino disponível",
|
||||
"image": "Imagem",
|
||||
"format": "Formato",
|
||||
"dimensions": "Dimensões",
|
||||
"segments": "Segmentos",
|
||||
"transfers": "Transferências",
|
||||
"downloadedBy": "Transferido por",
|
||||
"saveDiscoverySettings": "Guardar definições de descoberta",
|
||||
"savePublicInfo": "Guardar informação pública",
|
||||
"saveRadioSettings": "Guardar definições de rádio",
|
||||
"savePath": "Guardar caminho",
|
||||
"wipeDeviceData": "Apagar dados do dispositivo",
|
||||
"wipeDevice": "Apagar dispositivo",
|
||||
"destructiveDeviceActions": "Ações destrutivas do dispositivo.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Escolha uma predefinição ou ajuste as definições de rádio.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Escolha o nome e a localização que este dispositivo partilha.",
|
||||
"availableSpaceOnThisDevice": "Espaço disponível neste dispositivo.",
|
||||
"used": "Usado",
|
||||
"total": "Total",
|
||||
"renameValue": "Renomear valor",
|
||||
"customizeFields": "Personalizar campos",
|
||||
"livePreview": "Pré-visualização ao vivo",
|
||||
"refreshSchedule": "Agenda de atualização",
|
||||
"noResponse": "Sem resposta",
|
||||
"refreshing": "A atualizar",
|
||||
"unavailable": "Indisponível",
|
||||
"pickARelayOrNodeToWatchInSensors": "Escolha um relay ou nó para observar.",
|
||||
"publicKeyLabel": "Chave pública",
|
||||
"alreadyInContacts": "Já nos contactos",
|
||||
"connectToADeviceBeforeAddingContacts": "Conecte-se a um dispositivo antes de adicionar contactos",
|
||||
"fromContacts": "Dos contactos",
|
||||
"onlineOnly": "Apenas online",
|
||||
"inBoth": "Em ambos",
|
||||
"source": "Fonte",
|
||||
"manualRouteEdit": "Edição manual de rota",
|
||||
"observedMeshRoute": "Rota mesh observada",
|
||||
"allMessagesCleared": "Todas as mensagens limpas",
|
||||
"onlineTraceDatabaseCleared": "Base de dados de rastreio limpa",
|
||||
"packetLogsCleared": "Registos de pacotes limpos",
|
||||
"hexDataCopiedToClipboard": "Dados hex copiados para a área de transferência",
|
||||
"developerModeEnabled": "Modo de programador ativado",
|
||||
"developerModeDisabled": "Modo de programador desativado",
|
||||
"clipboardIsEmpty": "A área de transferência está vazia",
|
||||
"contactImported": "Contacto importado",
|
||||
"contactLinkCopiedToClipboard": "Link do contacto copiado para a área de transferência",
|
||||
"failedToExportContact": "Falha ao exportar contacto",
|
||||
"noLogsToExport": "Sem registos para exportar",
|
||||
"exportAsCsv": "Exportar como CSV",
|
||||
"exportAsText": "Exportar como texto",
|
||||
"receivedRfc3339": "Recebido (RFC3339)",
|
||||
"buildTime": "Hora de compilação",
|
||||
"downloadUrlNotAvailable": "URL de download não disponível",
|
||||
"cannotOpenDownloadUrl": "Não é possível abrir o URL de download",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "A verificação de atualizações só está disponível no Android",
|
||||
"youAreRunningTheLatestVersion": "Está a usar a versão mais recente",
|
||||
"updateAvailableButDownloadUrlNotFound": "Atualização disponível mas URL de download não encontrado",
|
||||
"startTictactoe": "Iniciar Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe indisponível",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: oponente desconhecido",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: a aguardar início",
|
||||
"acceptsShareLinks": "Aceita links partilhados",
|
||||
"supportsRawHex": "Suporta hex bruto",
|
||||
"clipboardfriendly": "Compatível com a área de transferência",
|
||||
"captured": "Capturado",
|
||||
"size": "Tamanho",
|
||||
"noCustomChannelsToClear": "Sem canais personalizados para limpar.",
|
||||
"noDeviceContactsToClear": "Sem contactos do dispositivo para limpar."
|
||||
}
|
||||
|
||||
@@ -511,5 +511,304 @@
|
||||
"rawDump": "Необработанные данные",
|
||||
"cannotRetryMissingRecipient": "Повтор невозможен: нет информации о получателе",
|
||||
"voiceUnavailable": "Голос сейчас недоступен",
|
||||
"requestingVoice": "Запрос голоса"
|
||||
"requestingVoice": "Запрос голоса",
|
||||
"device": "устройство",
|
||||
"change": "Изменить",
|
||||
"wizardOverviewDescription": "Это приложение объединяет сообщения MeshCore, полевые обновления SAR, карты и инструменты устройства в одном месте.",
|
||||
"wizardOverviewFeature1": "Отправляйте личные сообщения, сообщения комнат и сообщения каналов из основной вкладки «Сообщения».",
|
||||
"wizardOverviewFeature2": "Делитесь SAR-маркерами, рисунками на карте, голосовыми клипами и изображениями через mesh-сеть.",
|
||||
"wizardOverviewFeature3": "Подключайтесь по BLE или TCP, а затем управляйте сопутствующей радиостанцией прямо из приложения.",
|
||||
"wizardMessagingTitle": "Сообщения и полевые отчёты",
|
||||
"wizardMessagingDescription": "Здесь сообщения это больше, чем просто текст. Приложение уже поддерживает несколько видов операционных данных и сценариев передачи.",
|
||||
"wizardMessagingFeature1": "Отправляйте личные сообщения, сообщения комнат и трафик каналов из одного редактора.",
|
||||
"wizardMessagingFeature2": "Создавайте обновления SAR и повторно используемые шаблоны SAR для типовых полевых отчётов.",
|
||||
"wizardMessagingFeature3": "Передавайте голосовые сессии и изображения с индикатором прогресса и оценкой эфирного времени в интерфейсе.",
|
||||
"wizardConnectDeviceTitle": "Подключить устройство",
|
||||
"wizardConnectDeviceDescription": "Подключите свою радиостанцию MeshCore, выберите имя и примените радиопрофиль перед продолжением.",
|
||||
"wizardSetupBadge": "Настройка",
|
||||
"wizardOverviewBadge": "Обзор",
|
||||
"wizardConnectedToDevice": "Подключено к {deviceName}",
|
||||
"wizardNoDeviceConnected": "Устройство ещё не подключено",
|
||||
"wizardSkipForNow": "Пропустить пока",
|
||||
"wizardDeviceNameLabel": "Имя устройства",
|
||||
"wizardDeviceNameHelp": "Это имя транслируется другим пользователям MeshCore.",
|
||||
"wizardConfigRegionLabel": "Регион конфигурации",
|
||||
"wizardConfigRegionHelp": "Используется полный официальный список профилей MeshCore. По умолчанию выбран EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Убедитесь, что выбранный профиль соответствует местным радиоправилам.",
|
||||
"wizardPresetNote2": "Список соответствует официальному потоку профилей инструмента MeshCore config.",
|
||||
"wizardPresetNote3": "Для онбординга по умолчанию остаётся выбран EU/UK (Narrow).",
|
||||
"wizardSaving": "Сохранение...",
|
||||
"wizardSaveAndContinue": "Сохранить и продолжить",
|
||||
"wizardEnterDeviceName": "Введите имя устройства перед продолжением.",
|
||||
"wizardDeviceSetupSaved": "Сохранено: {deviceName}, профиль {presetName}.",
|
||||
"wizardNetworkTitle": "Контакты, комнаты и ретрансляторы",
|
||||
"wizardNetworkDescription": "Вкладка «Контакты» организует сеть, которую вы обнаруживаете, и маршруты, которые вы узнаёте со временем.",
|
||||
"wizardNetworkFeature1": "Просматривайте участников команды, ретрансляторы, комнаты, каналы и ожидающие объявления в одном списке.",
|
||||
"wizardNetworkFeature2": "Используйте smart ping, вход в комнаты, изученные пути и инструменты сброса маршрутов, когда связь становится нестабильной.",
|
||||
"wizardNetworkFeature3": "Создавайте каналы и управляйте сетевыми назначениями, не покидая приложение.",
|
||||
"wizardMapOpsTitle": "Карта, следы и общая геометрия",
|
||||
"wizardMapOpsDescription": "Карта приложения напрямую связана с сообщениями, отслеживанием и SAR-слоями, а не является отдельным просмотрщиком.",
|
||||
"wizardMapOpsFeature1": "Отслеживайте свою позицию, местоположение команды и траектории движения на карте.",
|
||||
"wizardMapOpsFeature2": "Открывайте рисунки из сообщений, просматривайте их прямо в интерфейсе и удаляйте с карты при необходимости.",
|
||||
"wizardMapOpsFeature3": "Используйте карты ретрансляторов и общие слои, чтобы понимать покрытие сети в поле.",
|
||||
"wizardToolsTitle": "Инструменты помимо сообщений",
|
||||
"wizardToolsDescription": "Здесь есть больше, чем четыре основные вкладки. Приложение также включает настройку, диагностику и необязательные сценарии датчиков.",
|
||||
"wizardToolsFeature1": "Откройте настройки устройства, чтобы изменить параметры радио, телеметрию, мощность TX и сведения о сопутствующем устройстве.",
|
||||
"wizardToolsFeature2": "Включите вкладку «Датчики», если нужны панели наблюдаемых датчиков и быстрые действия обновления.",
|
||||
"wizardToolsFeature3": "Используйте журналы пакетов, сканирование спектра и диагностику разработчика для устранения проблем mesh-сети.",
|
||||
"contactInSensors": "В датчиках",
|
||||
"contactAddToSensors": "Добавить в датчики",
|
||||
"contactSetPath": "Задать путь",
|
||||
"contactAddedToSensors": "{contactName} добавлен в Датчики",
|
||||
"contactFailedToClearRoute": "Не удалось очистить маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищен",
|
||||
"contactRouteSet": "Маршрут задан: {route}",
|
||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Таймаут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Полезная нагрузка",
|
||||
"hops": "Хопы",
|
||||
"hashSize": "Размер хеша",
|
||||
"pathBytes": "Байты пути",
|
||||
"selectedPath": "Выбранный путь",
|
||||
"estimatedTx": "Ожидаемая передача",
|
||||
"senderToReceipt": "От отправителя до получения",
|
||||
"receivedCopies": "Полученные копии",
|
||||
"retryCause": "Причина повтора",
|
||||
"retryMode": "Режим повтора",
|
||||
"retryResult": "Результат повтора",
|
||||
"lastRetry": "Последний повтор",
|
||||
"rxPackets": "RX-пакеты",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Скорость",
|
||||
"window": "Окно",
|
||||
"posttxDelay": "Задержка после передачи",
|
||||
"bandpass": "Полосовой",
|
||||
"bandpassFilterVoice": "Полосовой фильтр голоса",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралия",
|
||||
"australiaNarrow": "Австралия (Узкий)",
|
||||
"australiaQld": "Австралия: QLD",
|
||||
"australiaSaWa": "Австралия: SA, WA",
|
||||
"newZealand": "Новая Зеландия",
|
||||
"newZealandNarrow": "Новая Зеландия (Узкий)",
|
||||
"switzerland": "Швейцария",
|
||||
"portugal433": "Португалия 433",
|
||||
"portugal868": "Португалия 868",
|
||||
"czechRepublicNarrow": "Чехия (Узкий)",
|
||||
"eu433mhzLongRange": "ЕС 433МГц (Дальний)",
|
||||
"euukDeprecated": "ЕС/Великобритания (Устарело)",
|
||||
"euukNarrow": "ЕС/Великобритания (Узкий)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендуется)",
|
||||
"vietnamDeprecated": "Вьетнам (Устарело)",
|
||||
"vietnamNarrow": "Вьетнам (Узкий)",
|
||||
"active": "Активно",
|
||||
"addContact": "Добавить контакт",
|
||||
"all": "Все",
|
||||
"autoResolve": "Автоматическое разрешение",
|
||||
"clearAllLabel": "Очистить все",
|
||||
"clearRelays": "Очистить реле",
|
||||
"clearFilters": "Очистить фильтры",
|
||||
"clearRoute": "Очистить маршрут",
|
||||
"clearMessages": "Очистить сообщения",
|
||||
"clearScale": "Очистить масштаб",
|
||||
"clearDiscoveries": "Очистить обнаружения",
|
||||
"clearOnlineTraceDatabase": "Очистить базу трассировок",
|
||||
"clearAllChannels": "Очистить все каналы",
|
||||
"clearAllContacts": "Очистить все контакты",
|
||||
"clearChannels": "Очистить каналы",
|
||||
"clearContacts": "Очистить контакты",
|
||||
"clearPathOnMaxRetry": "Очистить путь при макс. повторе",
|
||||
"create": "Создать",
|
||||
"custom": "Пользовательский",
|
||||
"defaultValue": "По умолчанию",
|
||||
"duplicate": "Дублировать",
|
||||
"editName": "Редактировать имя",
|
||||
"open": "Открыть",
|
||||
"paste": "Вставить",
|
||||
"preview": "Предпросмотр",
|
||||
"remove": "Удалить",
|
||||
"rename": "Переименовать",
|
||||
"resolveAll": "Разрешить все",
|
||||
"send": "Отправить",
|
||||
"sendAnyway": "Отправить все равно",
|
||||
"share": "Поделиться",
|
||||
"shareContact": "Поделиться контактом",
|
||||
"trace": "Трассировка",
|
||||
"use": "Использовать",
|
||||
"useSelectedFrequency": "Использовать выбранную частоту",
|
||||
"discovery": "Обнаружение",
|
||||
"discoverRepeaters": "Обнаружить повторители",
|
||||
"discoverSensors": "Обнаружить датчики",
|
||||
"repeaterDiscoverySent": "Обнаружение повторителей отправлено",
|
||||
"sensorDiscoverySent": "Обнаружение датчиков отправлено",
|
||||
"clearedPendingDiscoveries": "Ожидающие обнаружения очищены.",
|
||||
"autoDiscovery": "Автоматическое обнаружение",
|
||||
"enableAutomaticAdding": "Включить автоматическое добавление",
|
||||
"autoaddRepeaters": "Автодобавление повторителей",
|
||||
"autoaddRoomServers": "Автодобавление серверов комнат",
|
||||
"autoaddSensors": "Автодобавление датчиков",
|
||||
"autoaddUsers": "Автодобавление пользователей",
|
||||
"overwriteOldestWhenFull": "Перезаписать старейшие при заполнении",
|
||||
"storage": "Хранилище",
|
||||
"dangerZone": "Опасная зона",
|
||||
"profiles": "Профили",
|
||||
"favourites": "Избранное",
|
||||
"sensors": "Датчики",
|
||||
"others": "Другие",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живой трафик",
|
||||
"repeatersMap": "Карта повторителей",
|
||||
"spectrumScan": "Сканирование спектра",
|
||||
"blePacketLogs": "Журналы BLE-пакетов",
|
||||
"onlineTraceDatabase": "База трассировок",
|
||||
"routePathByteSize": "Размер пути в байтах",
|
||||
"messageNotifications": "Уведомления о сообщениях",
|
||||
"sarAlerts": "SAR-оповещения",
|
||||
"discoveryNotifications": "Уведомления об обнаружении",
|
||||
"updateNotifications": "Уведомления об обновлениях",
|
||||
"muteWhileAppIsOpen": "Без звука при открытом приложении",
|
||||
"disableContacts": "Отключить контакты",
|
||||
"enableSensorsTab": "Включить вкладку Датчики",
|
||||
"enableProfiles": "Включить профили",
|
||||
"autoRouteRotation": "Автоматическая ротация маршрута",
|
||||
"nearestRepeaterFallback": "Ближайший повторитель как резерв",
|
||||
"deleteAllStoredMessageHistory": "Удалить всю историю сообщений",
|
||||
"messageFontSize": "Размер шрифта сообщений",
|
||||
"rotateMapWithHeading": "Вращать карту по направлению",
|
||||
"showMapDebugInfo": "Показать отладочную информацию карты",
|
||||
"openMapInFullscreen": "Открыть карту на весь экран",
|
||||
"showSarMarkersLabel": "Показать SAR-маркеры",
|
||||
"displaySarMarkersOnTheMainMap": "Отображать SAR-маркеры на основной карте",
|
||||
"showAllContactTrailsLabel": "Показать все следы контактов",
|
||||
"hideRepeatersOnMap": "Скрыть повторители на карте",
|
||||
"setMapScale": "Установить масштаб карты",
|
||||
"customMapScaleSaved": "Пользовательский масштаб карты сохранён",
|
||||
"voiceBitrate": "Битрейт голоса",
|
||||
"voiceCompressor": "Компрессор голоса",
|
||||
"balancesQuietAndLoudSpeechLevels": "Выравнивает тихую и громкую речь",
|
||||
"voiceLimiter": "Лимитер голоса",
|
||||
"preventsClippingPeaksBeforeEncoding": "Предотвращает обрезку пиков перед кодированием",
|
||||
"micAutoGain": "Автоусиление микрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Позволяет рекордеру регулировать уровень входа",
|
||||
"echoCancellation": "Подавление эха",
|
||||
"noiseSuppression": "Подавление шума",
|
||||
"trimSilenceInVoiceMessages": "Обрезать тишину в голосовых сообщениях",
|
||||
"compressor": "Компрессор",
|
||||
"limiter": "Лимитер",
|
||||
"autoGain": "Автоусиление",
|
||||
"echoCancel": "Эхо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тишина",
|
||||
"maxImageSize": "Максимальный размер изображения",
|
||||
"imageCompression": "Сжатие изображения",
|
||||
"grayscale": "Оттенки серого",
|
||||
"ultraMode": "Режим ультра",
|
||||
"fastPrivateGpsUpdates": "Быстрые приватные GPS-обновления",
|
||||
"movementThreshold": "Порог движения",
|
||||
"fastGpsMovementThreshold": "Порог движения быстрого GPS",
|
||||
"fastGpsActiveuseInterval": "Интервал активного использования быстрого GPS",
|
||||
"activeuseUpdateInterval": "Интервал обновления при активном использовании",
|
||||
"repeatNearbyTraffic": "Повторять ближний трафик",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслировать через повторители по сети",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Только рядом, без flood повторителей",
|
||||
"multihop": "Многохоповый",
|
||||
"createProfile": "Создать профиль",
|
||||
"renameProfile": "Переименовать профиль",
|
||||
"newProfile": "Новый профиль",
|
||||
"manageProfiles": "Управление профилями",
|
||||
"enableProfilesToStartManagingThem": "Включите профили, чтобы начать управлять ими.",
|
||||
"openMessage": "Открыть сообщение",
|
||||
"jumpToTheRelatedSarMessage": "Перейти к связанному SAR-сообщению",
|
||||
"removeSarMarker": "Удалить SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Выберите назначение для отправки SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер отправлен в публичный канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер отправлен в комнату",
|
||||
"loadFromGallery": "Загрузить из галереи",
|
||||
"replaceImage": "Заменить изображение",
|
||||
"selectFromGallery": "Выбрать из галереи",
|
||||
"team": "Команда",
|
||||
"found": "Найдено",
|
||||
"staging": "Место сбора",
|
||||
"object": "Объект",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Умеренно",
|
||||
"busy": "Занято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканирование спектра не нашло частот-кандидатов",
|
||||
"searchMessages": "Поиск сообщений",
|
||||
"sendImageFromGallery": "Отправить изображение из галереи",
|
||||
"takePhoto": "Сделать фото",
|
||||
"dmOnly": "Только личные сообщения",
|
||||
"allMessages": "Все сообщения",
|
||||
"sendToPublicChannel": "Отправить в публичный канал?",
|
||||
"selectMarkerTypeAndDestination": "Выберите тип маркера и назначение",
|
||||
"noDestinationsAvailableLabel": "Нет доступных назначений",
|
||||
"image": "Изображение",
|
||||
"format": "Формат",
|
||||
"dimensions": "Размеры",
|
||||
"segments": "Сегменты",
|
||||
"transfers": "Передачи",
|
||||
"downloadedBy": "Загружено",
|
||||
"saveDiscoverySettings": "Сохранить настройки обнаружения",
|
||||
"savePublicInfo": "Сохранить публичную информацию",
|
||||
"saveRadioSettings": "Сохранить настройки радио",
|
||||
"savePath": "Сохранить путь",
|
||||
"wipeDeviceData": "Стереть данные устройства",
|
||||
"wipeDevice": "Стереть устройство",
|
||||
"destructiveDeviceActions": "Деструктивные действия устройства.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Выберите пресет или настройте радио вручную.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Выберите имя и местоположение, которыми делится устройство.",
|
||||
"availableSpaceOnThisDevice": "Доступное место на этом устройстве.",
|
||||
"used": "Использовано",
|
||||
"total": "Всего",
|
||||
"renameValue": "Переименовать значение",
|
||||
"customizeFields": "Настроить поля",
|
||||
"livePreview": "Предпросмотр в реальном времени",
|
||||
"refreshSchedule": "Расписание обновления",
|
||||
"noResponse": "Нет ответа",
|
||||
"refreshing": "Обновление",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Выберите реле или узел для наблюдения.",
|
||||
"publicKeyLabel": "Публичный ключ",
|
||||
"alreadyInContacts": "Уже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Подключитесь к устройству перед добавлением контактов",
|
||||
"fromContacts": "Из контактов",
|
||||
"onlineOnly": "Только онлайн",
|
||||
"inBoth": "В обоих",
|
||||
"source": "Источник",
|
||||
"manualRouteEdit": "Ручное редактирование маршрута",
|
||||
"observedMeshRoute": "Наблюдаемый mesh-маршрут",
|
||||
"allMessagesCleared": "Все сообщения очищены",
|
||||
"onlineTraceDatabaseCleared": "База трассировок очищена",
|
||||
"packetLogsCleared": "Журналы пакетов очищены",
|
||||
"hexDataCopiedToClipboard": "Hex-данные скопированы в буфер",
|
||||
"developerModeEnabled": "Режим разработчика включён",
|
||||
"developerModeDisabled": "Режим разработчика отключён",
|
||||
"clipboardIsEmpty": "Буфер обмена пуст",
|
||||
"contactImported": "Контакт импортирован",
|
||||
"contactLinkCopiedToClipboard": "Ссылка на контакт скопирована в буфер",
|
||||
"failedToExportContact": "Не удалось экспортировать контакт",
|
||||
"noLogsToExport": "Нет журналов для экспорта",
|
||||
"exportAsCsv": "Экспортировать как CSV",
|
||||
"exportAsText": "Экспортировать как текст",
|
||||
"receivedRfc3339": "Получено (RFC3339)",
|
||||
"buildTime": "Время сборки",
|
||||
"downloadUrlNotAvailable": "URL загрузки недоступен",
|
||||
"cannotOpenDownloadUrl": "Не удаётся открыть URL загрузки",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Проверка обновлений доступна только на Android",
|
||||
"youAreRunningTheLatestVersion": "Вы используете последнюю версию",
|
||||
"updateAvailableButDownloadUrlNotFound": "Обновление доступно, но URL загрузки не найден",
|
||||
"startTictactoe": "Начать Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: противник неизвестен",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: ожидание начала",
|
||||
"acceptsShareLinks": "Принимает общие ссылки",
|
||||
"supportsRawHex": "Поддерживает raw hex",
|
||||
"clipboardfriendly": "Удобно для буфера",
|
||||
"captured": "Захвачено",
|
||||
"size": "Размер",
|
||||
"noCustomChannelsToClear": "Нет пользовательских каналов для очистки.",
|
||||
"noDeviceContactsToClear": "Нет контактов устройства для очистки."
|
||||
}
|
||||
|
||||
@@ -511,5 +511,304 @@
|
||||
"rawDump": "Surovi izpis",
|
||||
"cannotRetryMissingRecipient": "Ponovnega pošiljanja ni mogoče: informacije o prejemniku manjkajo",
|
||||
"voiceUnavailable": "Glas trenutno ni na voljo",
|
||||
"requestingVoice": "Zahteva za glasom"
|
||||
"requestingVoice": "Zahteva za glasom",
|
||||
"device": "naprava",
|
||||
"change": "Spremeni",
|
||||
"wizardOverviewDescription": "Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.",
|
||||
"wizardOverviewFeature1": "Pošiljajte neposredna sporočila, objave v sobah in sporočila na kanalih iz glavnega zavihka Sporočila.",
|
||||
"wizardOverviewFeature2": "Delite SAR označevalce, risbe zemljevida, glasovne posnetke in slike prek mesh omrežja.",
|
||||
"wizardOverviewFeature3": "Povežite se prek BLE ali TCP in nato upravljajte spremljevalni radio kar iz aplikacije.",
|
||||
"wizardMessagingTitle": "Sporočanje in terenska poročila",
|
||||
"wizardMessagingDescription": "Sporočila tukaj niso le navadno besedilo. Aplikacija že podpira več operativnih vrst vsebin in prenosnih tokov.",
|
||||
"wizardMessagingFeature1": "Pošiljajte neposredna sporočila, objave v sobah in promet na kanalih iz enega urejevalnika.",
|
||||
"wizardMessagingFeature2": "Ustvarjajte SAR posodobitve in večkrat uporabne SAR predloge za pogosta terenska poročila.",
|
||||
"wizardMessagingFeature3": "Prenašajte glasovne seje in slike, vmesnik pa prikazuje napredek in oceno časa prenosa.",
|
||||
"wizardConnectDeviceTitle": "Poveži napravo",
|
||||
"wizardConnectDeviceDescription": "Povežite svoj MeshCore radio, izberite ime in uporabite radijski prednastavljeni profil, preden nadaljujete.",
|
||||
"wizardSetupBadge": "Nastavitev",
|
||||
"wizardOverviewBadge": "Pregled",
|
||||
"wizardConnectedToDevice": "Povezano z: {deviceName}",
|
||||
"wizardNoDeviceConnected": "Nobena naprava še ni povezana",
|
||||
"wizardSkipForNow": "Za zdaj preskoči",
|
||||
"wizardDeviceNameLabel": "Ime naprave",
|
||||
"wizardDeviceNameHelp": "To ime se oglašuje drugim uporabnikom MeshCore.",
|
||||
"wizardConfigRegionLabel": "Konfiguracijska regija",
|
||||
"wizardConfigRegionHelp": "Uporabi celoten uradni seznam prednastavitev MeshCore. Privzeto je izbran EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Prepričajte se, da izbrana prednastavitev ustreza lokalnim radijskim predpisom.",
|
||||
"wizardPresetNote2": "Seznam ustreza uradnemu viru prednastavitev orodja MeshCore config.",
|
||||
"wizardPresetNote3": "Za uvajanje ostane privzeto izbran EU/UK (Narrow).",
|
||||
"wizardSaving": "Shranjujem...",
|
||||
"wizardSaveAndContinue": "Shrani in nadaljuj",
|
||||
"wizardEnterDeviceName": "Pred nadaljevanjem vnesite ime naprave.",
|
||||
"wizardDeviceSetupSaved": "Shranjeno {deviceName} z nastavitvijo {presetName}.",
|
||||
"wizardNetworkTitle": "Stiki, sobe in repetitorji",
|
||||
"wizardNetworkDescription": "Zavihek Stiki organizira omrežje, ki ga odkrijete, in poti, ki se jih aplikacija nauči skozi čas.",
|
||||
"wizardNetworkFeature1": "Na enem seznamu preglejte člane ekipe, repetitorje, sobe, kanale in čakajoče oglase.",
|
||||
"wizardNetworkFeature2": "Uporabite smart ping, prijavo v sobe, naučene poti in ponastavitev poti, ko povezljivost postane neurejena.",
|
||||
"wizardNetworkFeature3": "Ustvarjajte kanale in upravljajte omrežne cilje, ne da bi zapustili aplikacijo.",
|
||||
"wizardMapOpsTitle": "Zemljevid, sledi in deljena geometrija",
|
||||
"wizardMapOpsDescription": "Zemljevid v aplikaciji je neposredno povezan s sporočanjem, sledenjem in SAR prekrivanji, namesto da bi bil le ločen pregledovalnik.",
|
||||
"wizardMapOpsFeature1": "Spremljajte svoj položaj, lokacije sotekmovalcev in gibanje sledi na zemljevidu.",
|
||||
"wizardMapOpsFeature2": "Odprite risbe iz sporočil, jih predoglejte v vrstici in jih po potrebi odstranite z zemljevida.",
|
||||
"wizardMapOpsFeature3": "Uporabite poglede repetitorjev in deljena prekrivanja za razumevanje dosega omrežja na terenu.",
|
||||
"wizardToolsTitle": "Orodja onkraj sporočanja",
|
||||
"wizardToolsDescription": "Tu je več kot le štirje glavni zavihki. Aplikacija vključuje tudi konfiguracijo, diagnostiko in neobvezne senzorske poteke.",
|
||||
"wizardToolsFeature1": "Odprite nastavitve naprave za spremembo radijskih nastavitev, telemetrije, oddajne moči in podatkov spremljevalne naprave.",
|
||||
"wizardToolsFeature2": "Omogočite zavihek Senzorji, kadar želite nadzorne plošče in hitra osveževanja spremljanih senzorjev.",
|
||||
"wizardToolsFeature3": "Uporabite dnevnik paketov, pregled spektra in razvojno diagnostiko pri odpravljanju težav v mesh omrežju.",
|
||||
"contactInSensors": "V senzorjih",
|
||||
"contactAddToSensors": "Dodaj v senzorje",
|
||||
"contactSetPath": "Nastavi pot",
|
||||
"contactAddedToSensors": "{contactName} dodan v senzorje",
|
||||
"contactFailedToClearRoute": "Brisanje poti ni uspelo: {error}",
|
||||
"contactRouteCleared": "Pot izbrisana",
|
||||
"contactRouteSet": "Pot nastavljena: {route}",
|
||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Časovna omejitev ACK",
|
||||
"opcode": "Opkoda",
|
||||
"payload": "Vsebina",
|
||||
"hops": "Skoki",
|
||||
"hashSize": "Velikost zgoščevalne vrednosti",
|
||||
"pathBytes": "Bajti poti",
|
||||
"selectedPath": "Izbrana pot",
|
||||
"estimatedTx": "Ocenjeni čas oddaje",
|
||||
"senderToReceipt": "Od pošiljatelja do prejema",
|
||||
"receivedCopies": "Prejete kopije",
|
||||
"retryCause": "Razlog ponovitve",
|
||||
"retryMode": "Način ponovitve",
|
||||
"retryResult": "Rezultat ponovitve",
|
||||
"lastRetry": "Zadnja ponovitev",
|
||||
"rxPackets": "RX paketi",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hitrost",
|
||||
"window": "Okno",
|
||||
"posttxDelay": "Zakasnitev po oddaji",
|
||||
"bandpass": "Pasovno sito",
|
||||
"bandpassFilterVoice": "Pasovno sito za glas",
|
||||
"frequency": "Frekvenca",
|
||||
"australia": "Avstralija",
|
||||
"australiaNarrow": "Avstralija (ozko)",
|
||||
"australiaQld": "Avstralija: QLD",
|
||||
"australiaSaWa": "Avstralija: SA, WA",
|
||||
"newZealand": "Nova Zelandija",
|
||||
"newZealandNarrow": "Nova Zelandija (ozko)",
|
||||
"switzerland": "Švica",
|
||||
"portugal433": "Portugalska 433",
|
||||
"portugal868": "Portugalska 868",
|
||||
"czechRepublicNarrow": "Češka (ozko)",
|
||||
"eu433mhzLongRange": "EU 433MHz (dolg doseg)",
|
||||
"euukDeprecated": "EU/UK (zastarelo)",
|
||||
"euukNarrow": "EU/UK (ozko)",
|
||||
"usacanadaRecommended": "ZDA/Kanada (priporočeno)",
|
||||
"vietnamDeprecated": "Vietnam (zastarelo)",
|
||||
"vietnamNarrow": "Vietnam (ozko)",
|
||||
"active": "Aktivno",
|
||||
"addContact": "Dodaj stik",
|
||||
"all": "Vse",
|
||||
"autoResolve": "Samodejno razreši",
|
||||
"clearAllLabel": "Počisti vse",
|
||||
"clearRelays": "Počisti posrednike",
|
||||
"clearFilters": "Počisti filtre",
|
||||
"clearRoute": "Počisti pot",
|
||||
"clearMessages": "Počisti sporočila",
|
||||
"clearScale": "Počisti merilo",
|
||||
"clearDiscoveries": "Počisti odkritja",
|
||||
"clearOnlineTraceDatabase": "Počisti bazo sledi",
|
||||
"clearAllChannels": "Počisti vse kanale",
|
||||
"clearAllContacts": "Počisti vse stike",
|
||||
"clearChannels": "Počisti kanale",
|
||||
"clearContacts": "Počisti stike",
|
||||
"clearPathOnMaxRetry": "Počisti pot ob maks. ponovitvi",
|
||||
"create": "Ustvari",
|
||||
"custom": "Po meri",
|
||||
"defaultValue": "Privzeto",
|
||||
"duplicate": "Podvoji",
|
||||
"editName": "Uredi ime",
|
||||
"open": "Odpri",
|
||||
"paste": "Prilepi",
|
||||
"preview": "Predogled",
|
||||
"remove": "Odstrani",
|
||||
"rename": "Preimenuj",
|
||||
"resolveAll": "Razreši vse",
|
||||
"send": "Pošlji",
|
||||
"sendAnyway": "Vseeno pošlji",
|
||||
"share": "Deli",
|
||||
"shareContact": "Deli stik",
|
||||
"trace": "Sledenje",
|
||||
"use": "Uporabi",
|
||||
"useSelectedFrequency": "Uporabi izbrano frekvenco",
|
||||
"discovery": "Odkrivanje",
|
||||
"discoverRepeaters": "Odkrij posrednike",
|
||||
"discoverSensors": "Odkrij senzorje",
|
||||
"repeaterDiscoverySent": "Odkrivanje posrednikov poslano",
|
||||
"sensorDiscoverySent": "Odkrivanje senzorjev poslano",
|
||||
"clearedPendingDiscoveries": "Čakalna odkritja počiščena.",
|
||||
"autoDiscovery": "Samodejno odkrivanje",
|
||||
"enableAutomaticAdding": "Omogoči samodejno dodajanje",
|
||||
"autoaddRepeaters": "Samodejno dodaj posrednike",
|
||||
"autoaddRoomServers": "Samodejno dodaj strežnike sob",
|
||||
"autoaddSensors": "Samodejno dodaj senzorje",
|
||||
"autoaddUsers": "Samodejno dodaj uporabnike",
|
||||
"overwriteOldestWhenFull": "Prepiši najstarejše ob polnem",
|
||||
"storage": "Shramba",
|
||||
"dangerZone": "Nevarno območje",
|
||||
"profiles": "Profili",
|
||||
"favourites": "Priljubljeni",
|
||||
"sensors": "Senzorji",
|
||||
"others": "Ostali",
|
||||
"gpsModule": "GPS modul",
|
||||
"liveTraffic": "Promet v živo",
|
||||
"repeatersMap": "Zemljevid posrednikov",
|
||||
"spectrumScan": "Spektralno skeniranje",
|
||||
"blePacketLogs": "Dnevniki BLE paketov",
|
||||
"onlineTraceDatabase": "Baza sledi",
|
||||
"routePathByteSize": "Velikost poti v bajtih",
|
||||
"messageNotifications": "Obvestila o sporočilih",
|
||||
"sarAlerts": "SAR opozorila",
|
||||
"discoveryNotifications": "Obvestila o odkritjih",
|
||||
"updateNotifications": "Obvestila o posodobitvah",
|
||||
"muteWhileAppIsOpen": "Utišaj ko je aplikacija odprta",
|
||||
"disableContacts": "Onemogoči stike",
|
||||
"enableSensorsTab": "Omogoči zavihek Senzorji",
|
||||
"enableProfiles": "Omogoči profile",
|
||||
"autoRouteRotation": "Samodejno kroženje poti",
|
||||
"nearestRepeaterFallback": "Najbližji posrednik kot rezerva",
|
||||
"deleteAllStoredMessageHistory": "Izbriši vso shranjeno zgodovino sporočil",
|
||||
"messageFontSize": "Velikost pisave sporočil",
|
||||
"rotateMapWithHeading": "Vrti zemljevid s smerjo",
|
||||
"showMapDebugInfo": "Prikaži razhroščevalne info zemljevida",
|
||||
"openMapInFullscreen": "Odpri zemljevid na cel zaslon",
|
||||
"showSarMarkersLabel": "Prikaži SAR oznake",
|
||||
"displaySarMarkersOnTheMainMap": "Prikaži SAR oznake na glavnem zemljevidu",
|
||||
"showAllContactTrailsLabel": "Prikaži vse sledi stikov",
|
||||
"hideRepeatersOnMap": "Skrij posrednike na zemljevidu",
|
||||
"setMapScale": "Nastavi merilo zemljevida",
|
||||
"customMapScaleSaved": "Merilo zemljevida shranjeno",
|
||||
"voiceBitrate": "Bitna hitrost glasu",
|
||||
"voiceCompressor": "Kompresor glasu",
|
||||
"balancesQuietAndLoudSpeechLevels": "Izravnava tihe in glasne govorne ravni",
|
||||
"voiceLimiter": "Omejevalnik glasu",
|
||||
"preventsClippingPeaksBeforeEncoding": "Preprečuje rezanje vrhov pred kodiranjem",
|
||||
"micAutoGain": "Samodejna ojačitev mikrofona",
|
||||
"letsTheRecorderAdjustInputLevel": "Omogoči snemalniku prilagoditev vhodne ravni",
|
||||
"echoCancellation": "Odstranjevanje odmeva",
|
||||
"noiseSuppression": "Dušenje šuma",
|
||||
"trimSilenceInVoiceMessages": "Obreži tišino v glasovnih sporočilih",
|
||||
"compressor": "Kompresor",
|
||||
"limiter": "Omejevalnik",
|
||||
"autoGain": "Samodejna ojačitev",
|
||||
"echoCancel": "Odmev",
|
||||
"noiseSuppress": "Šum",
|
||||
"silenceTrim": "Tišina",
|
||||
"maxImageSize": "Največja velikost slike",
|
||||
"imageCompression": "Stiskanje slik",
|
||||
"grayscale": "Sivine",
|
||||
"ultraMode": "Ultra način",
|
||||
"fastPrivateGpsUpdates": "Hitri zasebni GPS podatki",
|
||||
"movementThreshold": "Prag gibanja",
|
||||
"fastGpsMovementThreshold": "Prag gibanja za hitri GPS",
|
||||
"fastGpsActiveuseInterval": "Interval uporabe hitrega GPS-a",
|
||||
"activeuseUpdateInterval": "Interval posodobitve pri aktivni uporabi",
|
||||
"repeatNearbyTraffic": "Ponavljaj bližnji promet",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Posreduj skozi posrednike po mreži",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Samo bližnji, brez poplavljanja posrednikov",
|
||||
"multihop": "Večskočno",
|
||||
"createProfile": "Ustvari profil",
|
||||
"renameProfile": "Preimenuj profil",
|
||||
"newProfile": "Nov profil",
|
||||
"manageProfiles": "Upravljaj profile",
|
||||
"enableProfilesToStartManagingThem": "Omogočite profile, da jih začnete upravljati.",
|
||||
"openMessage": "Odpri sporočilo",
|
||||
"jumpToTheRelatedSarMessage": "Skoči na povezano SAR sporočilo",
|
||||
"removeSarMarker": "Odstrani SAR oznako",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Izberite cilj za pošiljanje SAR oznake",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR oznaka poslana na javni kanal",
|
||||
"sarMarkerSentToRoom": "SAR oznaka poslana v sobo",
|
||||
"loadFromGallery": "Naloži iz galerije",
|
||||
"replaceImage": "Zamenjaj sliko",
|
||||
"selectFromGallery": "Izberi iz galerije",
|
||||
"team": "Ekipa",
|
||||
"found": "Najdeno",
|
||||
"staging": "Zbirno mesto",
|
||||
"object": "Predmet",
|
||||
"quiet": "Tiho",
|
||||
"moderate": "Zmerno",
|
||||
"busy": "Zasedeno",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektralno skeniranje ni našlo primernih frekvenc",
|
||||
"searchMessages": "Iskanje sporočil",
|
||||
"sendImageFromGallery": "Pošlji sliko iz galerije",
|
||||
"takePhoto": "Posnemi fotografijo",
|
||||
"dmOnly": "Samo neposredno",
|
||||
"allMessages": "Vsa sporočila",
|
||||
"sendToPublicChannel": "Pošlji na javni kanal?",
|
||||
"selectMarkerTypeAndDestination": "Izberite vrsto oznake in cilj",
|
||||
"noDestinationsAvailableLabel": "Ni razpoložljivih ciljev",
|
||||
"image": "Slika",
|
||||
"format": "Format",
|
||||
"dimensions": "Dimenzije",
|
||||
"segments": "Segmenti",
|
||||
"transfers": "Prenosi",
|
||||
"downloadedBy": "Preneseno od",
|
||||
"saveDiscoverySettings": "Shrani nastavitve odkrivanja",
|
||||
"savePublicInfo": "Shrani javne podatke",
|
||||
"saveRadioSettings": "Shrani radijske nastavitve",
|
||||
"savePath": "Shrani pot",
|
||||
"wipeDeviceData": "Izbriši podatke naprave",
|
||||
"wipeDevice": "Izbriši napravo",
|
||||
"destructiveDeviceActions": "Destruktivna dejanja na napravi.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Izberite prednastavitev ali natančno prilagodite radijske nastavitve.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Izberite ime in lokacijo, ki ju naprava deli.",
|
||||
"availableSpaceOnThisDevice": "Razpoložljiv prostor na tej napravi.",
|
||||
"used": "Uporabljeno",
|
||||
"total": "Skupaj",
|
||||
"renameValue": "Preimenuj vrednost",
|
||||
"customizeFields": "Prilagodi polja",
|
||||
"livePreview": "Predogled v živo",
|
||||
"refreshSchedule": "Interval osvežitve",
|
||||
"noResponse": "Ni odgovora",
|
||||
"refreshing": "Osvežujem",
|
||||
"unavailable": "Nedostopno",
|
||||
"pickARelayOrNodeToWatchInSensors": "Izberite posrednika ali vozlišče za opazovanje.",
|
||||
"publicKeyLabel": "Javni ključ",
|
||||
"alreadyInContacts": "Že med stiki",
|
||||
"connectToADeviceBeforeAddingContacts": "Povežite se z napravo pred dodajanjem stikov",
|
||||
"fromContacts": "Iz stikov",
|
||||
"onlineOnly": "Samo povezani",
|
||||
"inBoth": "V obeh",
|
||||
"source": "Vir",
|
||||
"manualRouteEdit": "Ročno urejanje poti",
|
||||
"observedMeshRoute": "Opazovana mrežna pot",
|
||||
"allMessagesCleared": "Vsa sporočila počiščena",
|
||||
"onlineTraceDatabaseCleared": "Baza sledi počiščena",
|
||||
"packetLogsCleared": "Dnevniki paketov počiščeni",
|
||||
"hexDataCopiedToClipboard": "Hex podatki kopirani v odložišče",
|
||||
"developerModeEnabled": "Razvojni način omogočen",
|
||||
"developerModeDisabled": "Razvojni način onemogočen",
|
||||
"clipboardIsEmpty": "Odložišče je prazno",
|
||||
"contactImported": "Stik uvožen",
|
||||
"contactLinkCopiedToClipboard": "Povezava na stik kopirana v odložišče",
|
||||
"failedToExportContact": "Izvoz stika ni uspel",
|
||||
"noLogsToExport": "Ni dnevnikov za izvoz",
|
||||
"exportAsCsv": "Izvozi kot CSV",
|
||||
"exportAsText": "Izvozi kot besedilo",
|
||||
"receivedRfc3339": "Prejeto (RFC3339)",
|
||||
"buildTime": "Čas gradnje",
|
||||
"downloadUrlNotAvailable": "URL za prenos ni na voljo",
|
||||
"cannotOpenDownloadUrl": "URL za prenos ni mogoče odpreti",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Preverjanje posodobitev je na voljo le na Androidu",
|
||||
"youAreRunningTheLatestVersion": "Uporabljate najnovejšo različico",
|
||||
"updateAvailableButDownloadUrlNotFound": "Posodobitev na voljo, a URL za prenos ni najden",
|
||||
"startTictactoe": "Začni Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe ni na voljo",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: nasprotnik neznan",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: čakanje na začetek",
|
||||
"acceptsShareLinks": "Sprejema deljene povezave",
|
||||
"supportsRawHex": "Podpira surove hex",
|
||||
"clipboardfriendly": "Primerno za odložišče",
|
||||
"captured": "Zajeto",
|
||||
"size": "Velikost",
|
||||
"noCustomChannelsToClear": "Ni prilagojenih kanalov za brisanje.",
|
||||
"noDeviceContactsToClear": "Ni stikov naprave za brisanje."
|
||||
}
|
||||
|
||||
@@ -2276,5 +2276,304 @@
|
||||
"voiceUnavailable": "Ses şu anda kullanılamıyor",
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Ses isteniyor",
|
||||
"@requestingVoice": {}
|
||||
"@requestingVoice": {},
|
||||
"device": "cihaz",
|
||||
"change": "Değiştir",
|
||||
"wizardOverviewDescription": "Bu uygulama MeshCore mesajlaşmasını, SAR saha güncellemelerini, haritalamayı ve cihaz araçlarını tek bir yerde birleştirir.",
|
||||
"wizardOverviewFeature1": "Ana Mesajlar sekmesinden doğrudan mesajlar, oda gönderileri ve kanal mesajları gönderin.",
|
||||
"wizardOverviewFeature2": "Mesh üzerinden SAR işaretleri, harita çizimleri, ses klipleri ve görseller paylaşın.",
|
||||
"wizardOverviewFeature3": "BLE veya TCP ile bağlanın, ardından yardımcı radyoyu uygulamanın içinden yönetin.",
|
||||
"wizardMessagingTitle": "Mesajlaşma ve saha raporları",
|
||||
"wizardMessagingDescription": "Buradaki mesajlar düz metinden fazlasıdır. Uygulama zaten çeşitli operasyonel yükleri ve aktarım akışlarını destekliyor.",
|
||||
"wizardMessagingFeature1": "Doğrudan mesajları, oda gönderilerini ve kanal trafiğini tek bir düzenleyiciden gönderin.",
|
||||
"wizardMessagingFeature2": "Yaygın saha raporları için SAR güncellemeleri ve yeniden kullanılabilir SAR şablonları oluşturun.",
|
||||
"wizardMessagingFeature3": "Arayüzde ilerleme ve yayın süresi tahminleriyle ses oturumları ve görseller aktarın.",
|
||||
"wizardConnectDeviceTitle": "Cihazı bağla",
|
||||
"wizardConnectDeviceDescription": "MeshCore radyonuzu bağlayın, bir ad seçin ve devam etmeden önce bir radyo ön ayarı uygulayın.",
|
||||
"wizardSetupBadge": "Kurulum",
|
||||
"wizardOverviewBadge": "Genel bakış",
|
||||
"wizardConnectedToDevice": "{deviceName} cihazına bağlandı",
|
||||
"wizardNoDeviceConnected": "Henüz bağlı cihaz yok",
|
||||
"wizardSkipForNow": "Şimdilik atla",
|
||||
"wizardDeviceNameLabel": "Cihaz adı",
|
||||
"wizardDeviceNameHelp": "Bu ad diğer MeshCore kullanıcılarına duyurulur.",
|
||||
"wizardConfigRegionLabel": "Yapılandırma bölgesi",
|
||||
"wizardConfigRegionHelp": "MeshCore'un resmi ön ayar listesinin tamamını kullanır. Varsayılan EU/UK (Narrow) olur.",
|
||||
"wizardPresetNote1": "Seçilen ön ayarın yerel radyo düzenlemelerinize uygun olduğundan emin olun.",
|
||||
"wizardPresetNote2": "Liste, MeshCore yapılandırma aracının resmi ön ayar akışıyla eşleşir.",
|
||||
"wizardPresetNote3": "Onboarding sırasında varsayılan olarak EU/UK (Narrow) seçili kalır.",
|
||||
"wizardSaving": "Kaydediliyor...",
|
||||
"wizardSaveAndContinue": "Kaydet ve devam et",
|
||||
"wizardEnterDeviceName": "Devam etmeden önce bir cihaz adı girin.",
|
||||
"wizardDeviceSetupSaved": "{deviceName}, {presetName} ile kaydedildi.",
|
||||
"wizardNetworkTitle": "Kişiler, odalar ve tekrarlayıcılar",
|
||||
"wizardNetworkDescription": "Kişiler sekmesi keşfettiğiniz ağı ve zamanla öğrendiğiniz rotaları düzenler.",
|
||||
"wizardNetworkFeature1": "Ekip üyelerini, tekrarlayıcıları, odaları, kanalları ve bekleyen duyuruları tek listede inceleyin.",
|
||||
"wizardNetworkFeature2": "Bağlantı karmaşıklaştığında smart ping, oda girişi, öğrenilen yollar ve rota sıfırlama araçlarını kullanın.",
|
||||
"wizardNetworkFeature3": "Uygulamadan çıkmadan kanallar oluşturun ve ağ hedeflerini yönetin.",
|
||||
"wizardMapOpsTitle": "Harita, izler ve paylaşılan geometri",
|
||||
"wizardMapOpsDescription": "Uygulama haritası ayrı bir görüntüleyici olmak yerine mesajlaşma, takip ve SAR katmanlarıyla doğrudan bağlantılıdır.",
|
||||
"wizardMapOpsFeature1": "Kendi konumunuzu, ekip arkadaşlarının konumlarını ve hareket izlerini harita üzerinde takip edin.",
|
||||
"wizardMapOpsFeature2": "Mesajlardaki çizimleri açın, satır içinde önizleyin ve gerektiğinde haritadan kaldırın.",
|
||||
"wizardMapOpsFeature3": "Sahadaki ağ kapsamasını anlamak için tekrarlayıcı harita görünümleri ve paylaşılan katmanları kullanın.",
|
||||
"wizardToolsTitle": "Mesajlaşma dışındaki araçlar",
|
||||
"wizardToolsDescription": "Burada dört ana sekmeden fazlası var. Uygulama ayrıca yapılandırma, tanılama ve isteğe bağlı sensör akışları içerir.",
|
||||
"wizardToolsFeature1": "Radyo ayarlarını, telemetriyi, TX gücünü ve yardımcı cihaz ayrıntılarını değiştirmek için cihaz yapılandırmasını açın.",
|
||||
"wizardToolsFeature2": "İzlenen sensör panoları ve hızlı yenileme eylemleri istediğinizde Sensörler sekmesini etkinleştirin.",
|
||||
"wizardToolsFeature3": "Mesh sorunlarını giderirken paket günlükleri, spektrum taraması ve geliştirici tanılamalarını kullanın.",
|
||||
"contactInSensors": "Sensörlerde",
|
||||
"contactAddToSensors": "Sensörlere ekle",
|
||||
"contactSetPath": "Yol ayarla",
|
||||
"contactAddedToSensors": "{contactName} Sensörlere eklendi",
|
||||
"contactFailedToClearRoute": "Rota temizlenemedi: {error}",
|
||||
"contactRouteCleared": "Rota temizlendi",
|
||||
"contactRouteSet": "Rota ayarlandı: {route}",
|
||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK zaman aşımı",
|
||||
"opcode": "Opcode",
|
||||
"payload": "Yük",
|
||||
"hops": "Atlama",
|
||||
"hashSize": "Hash boyutu",
|
||||
"pathBytes": "Yol baytları",
|
||||
"selectedPath": "Seçilen yol",
|
||||
"estimatedTx": "Tahmini gönderim",
|
||||
"senderToReceipt": "Gönderenden alındıya",
|
||||
"receivedCopies": "Alınan kopyalar",
|
||||
"retryCause": "Yeniden deneme nedeni",
|
||||
"retryMode": "Yeniden deneme modu",
|
||||
"retryResult": "Yeniden deneme sonucu",
|
||||
"lastRetry": "Son deneme",
|
||||
"rxPackets": "RX paketleri",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Hız",
|
||||
"window": "Pencere",
|
||||
"posttxDelay": "Gönderim sonrası gecikme",
|
||||
"bandpass": "Bant geçiren",
|
||||
"bandpassFilterVoice": "Bant geçiren filtre ses",
|
||||
"frequency": "Frekans",
|
||||
"australia": "Avustralya",
|
||||
"australiaNarrow": "Avustralya (Dar)",
|
||||
"australiaQld": "Avustralya: QLD",
|
||||
"australiaSaWa": "Avustralya: SA, WA",
|
||||
"newZealand": "Yeni Zelanda",
|
||||
"newZealandNarrow": "Yeni Zelanda (Dar)",
|
||||
"switzerland": "İsviçre",
|
||||
"portugal433": "Portekiz 433",
|
||||
"portugal868": "Portekiz 868",
|
||||
"czechRepublicNarrow": "Çek Cumhuriyeti (Dar)",
|
||||
"eu433mhzLongRange": "AB 433MHz (Uzun Menzil)",
|
||||
"euukDeprecated": "AB/BK (Kullanımdan kalktı)",
|
||||
"euukNarrow": "AB/BK (Dar)",
|
||||
"usacanadaRecommended": "ABD/Kanada (Önerilen)",
|
||||
"vietnamDeprecated": "Vietnam (Kullanımdan kalktı)",
|
||||
"vietnamNarrow": "Vietnam (Dar)",
|
||||
"active": "Aktif",
|
||||
"addContact": "Kişi ekle",
|
||||
"all": "Tümü",
|
||||
"autoResolve": "Otomatik çöz",
|
||||
"clearAllLabel": "Tümünü temizle",
|
||||
"clearRelays": "Röleleri temizle",
|
||||
"clearFilters": "Filtreleri temizle",
|
||||
"clearRoute": "Rotayı temizle",
|
||||
"clearMessages": "Mesajları temizle",
|
||||
"clearScale": "Ölçeği temizle",
|
||||
"clearDiscoveries": "Keşifleri temizle",
|
||||
"clearOnlineTraceDatabase": "Çevrimiçi iz veritabanını temizle",
|
||||
"clearAllChannels": "Tüm kanalları temizle",
|
||||
"clearAllContacts": "Tüm kişileri temizle",
|
||||
"clearChannels": "Kanalları temizle",
|
||||
"clearContacts": "Kişileri temizle",
|
||||
"clearPathOnMaxRetry": "Maks denemede yolu temizle",
|
||||
"create": "Oluştur",
|
||||
"custom": "Özel",
|
||||
"defaultValue": "Varsayılan",
|
||||
"duplicate": "Çoğalt",
|
||||
"editName": "Adı düzenle",
|
||||
"open": "Aç",
|
||||
"paste": "Yapıştır",
|
||||
"preview": "Önizleme",
|
||||
"remove": "Kaldır",
|
||||
"rename": "Yeniden adlandır",
|
||||
"resolveAll": "Tümünü çöz",
|
||||
"send": "Gönder",
|
||||
"sendAnyway": "Yine de gönder",
|
||||
"share": "Paylaş",
|
||||
"shareContact": "Kişiyi paylaş",
|
||||
"trace": "İz",
|
||||
"use": "Kullan",
|
||||
"useSelectedFrequency": "Seçilen frekansı kullan",
|
||||
"discovery": "Keşif",
|
||||
"discoverRepeaters": "Tekrarlayıcıları keşfet",
|
||||
"discoverSensors": "Sensörleri keşfet",
|
||||
"repeaterDiscoverySent": "Tekrarlayıcı keşfi gönderildi",
|
||||
"sensorDiscoverySent": "Sensör keşfi gönderildi",
|
||||
"clearedPendingDiscoveries": "Bekleyen keşifler temizlendi.",
|
||||
"autoDiscovery": "Otomatik keşif",
|
||||
"enableAutomaticAdding": "Otomatik eklemeyi etkinleştir",
|
||||
"autoaddRepeaters": "Tekrarlayıcıları otomatik ekle",
|
||||
"autoaddRoomServers": "Oda sunucularını otomatik ekle",
|
||||
"autoaddSensors": "Sensörleri otomatik ekle",
|
||||
"autoaddUsers": "Kullanıcıları otomatik ekle",
|
||||
"overwriteOldestWhenFull": "Dolduğunda en eskiyi üzerine yaz",
|
||||
"storage": "Depolama",
|
||||
"dangerZone": "Tehlikeli bölge",
|
||||
"profiles": "Profiller",
|
||||
"favourites": "Favoriler",
|
||||
"sensors": "Sensörler",
|
||||
"others": "Diğerleri",
|
||||
"gpsModule": "GPS Modülü",
|
||||
"liveTraffic": "Canlı trafik",
|
||||
"repeatersMap": "Tekrarlayıcılar haritası",
|
||||
"spectrumScan": "Spektrum taraması",
|
||||
"blePacketLogs": "BLE paket günlükleri",
|
||||
"onlineTraceDatabase": "Çevrimiçi iz veritabanı",
|
||||
"routePathByteSize": "Yol bayt boyutu",
|
||||
"messageNotifications": "Mesaj bildirimleri",
|
||||
"sarAlerts": "SAR uyarıları",
|
||||
"discoveryNotifications": "Keşif bildirimleri",
|
||||
"updateNotifications": "Güncelleme bildirimleri",
|
||||
"muteWhileAppIsOpen": "Uygulama açıkken sessize al",
|
||||
"disableContacts": "Kişileri devre dışı bırak",
|
||||
"enableSensorsTab": "Sensörler sekmesini etkinleştir",
|
||||
"enableProfiles": "Profilleri etkinleştir",
|
||||
"autoRouteRotation": "Otomatik rota rotasyonu",
|
||||
"nearestRepeaterFallback": "En yakın tekrarlayıcıya geri dönüş",
|
||||
"deleteAllStoredMessageHistory": "Tüm mesaj geçmişini sil",
|
||||
"messageFontSize": "Mesaj yazı tipi boyutu",
|
||||
"rotateMapWithHeading": "Haritayı yön ile döndür",
|
||||
"showMapDebugInfo": "Harita hata ayıklama bilgisini göster",
|
||||
"openMapInFullscreen": "Haritayı tam ekranda aç",
|
||||
"showSarMarkersLabel": "SAR işaretçilerini göster",
|
||||
"displaySarMarkersOnTheMainMap": "SAR işaretçilerini ana haritada göster",
|
||||
"showAllContactTrailsLabel": "Tüm kişi izlerini göster",
|
||||
"hideRepeatersOnMap": "Haritada tekrarlayıcıları gizle",
|
||||
"setMapScale": "Harita ölçeğini ayarla",
|
||||
"customMapScaleSaved": "Özel harita ölçeği kaydedildi",
|
||||
"voiceBitrate": "Ses bit hızı",
|
||||
"voiceCompressor": "Ses sıkıştırıcı",
|
||||
"balancesQuietAndLoudSpeechLevels": "Sessiz ve yüksek konuşma seviyelerini dengeler",
|
||||
"voiceLimiter": "Ses sınırlayıcı",
|
||||
"preventsClippingPeaksBeforeEncoding": "Kodlama öncesi tepe kırpmasını önler",
|
||||
"micAutoGain": "Mikrofon otomatik kazancı",
|
||||
"letsTheRecorderAdjustInputLevel": "Kaydedicinin giriş seviyesini ayarlamasına izin verir",
|
||||
"echoCancellation": "Yankı iptali",
|
||||
"noiseSuppression": "Gürültü bastırma",
|
||||
"trimSilenceInVoiceMessages": "Sesli mesajlarda sessizliği kırp",
|
||||
"compressor": "Sıkıştırıcı",
|
||||
"limiter": "Sınırlayıcı",
|
||||
"autoGain": "Otomatik kazanç",
|
||||
"echoCancel": "Yankı",
|
||||
"noiseSuppress": "Gürültü",
|
||||
"silenceTrim": "Sessizlik",
|
||||
"maxImageSize": "Maksimum resim boyutu",
|
||||
"imageCompression": "Resim sıkıştırma",
|
||||
"grayscale": "Gri tonlama",
|
||||
"ultraMode": "Ultra mod",
|
||||
"fastPrivateGpsUpdates": "Hızlı özel GPS güncellemeleri",
|
||||
"movementThreshold": "Hareket eşiği",
|
||||
"fastGpsMovementThreshold": "Hızlı GPS hareket eşiği",
|
||||
"fastGpsActiveuseInterval": "Hızlı GPS aktif kullanım aralığı",
|
||||
"activeuseUpdateInterval": "Aktif kullanım güncelleme aralığı",
|
||||
"repeatNearbyTraffic": "Yakın trafiği tekrarla",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ağ üzerindeki tekrarlayıcılar aracılığıyla ilet",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Yalnızca yakın, tekrarlayıcı taşkını olmadan",
|
||||
"multihop": "Çok atlamalı",
|
||||
"createProfile": "Profil oluştur",
|
||||
"renameProfile": "Profili yeniden adlandır",
|
||||
"newProfile": "Yeni profil",
|
||||
"manageProfiles": "Profilleri yönet",
|
||||
"enableProfilesToStartManagingThem": "Profilleri yönetmeye başlamak için etkinleştirin.",
|
||||
"openMessage": "Mesajı aç",
|
||||
"jumpToTheRelatedSarMessage": "İlgili SAR mesajına git",
|
||||
"removeSarMarker": "SAR işaretçisini kaldır",
|
||||
"pleaseSelectADestinationToSendSarMarker": "SAR işaretçisi göndermek için bir hedef seçin",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR işaretçisi genel kanala yayınlandı",
|
||||
"sarMarkerSentToRoom": "SAR işaretçisi odaya gönderildi",
|
||||
"loadFromGallery": "Galeriden yükle",
|
||||
"replaceImage": "Resmi değiştir",
|
||||
"selectFromGallery": "Galeriden seç",
|
||||
"team": "Takım",
|
||||
"found": "Bulundu",
|
||||
"staging": "Toplanma alanı",
|
||||
"object": "Nesne",
|
||||
"quiet": "Sessiz",
|
||||
"moderate": "Orta",
|
||||
"busy": "Meşgul",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Spektrum taraması aday frekans bulamadı",
|
||||
"searchMessages": "Mesajlarda ara",
|
||||
"sendImageFromGallery": "Galeriden resim gönder",
|
||||
"takePhoto": "Fotoğraf çek",
|
||||
"dmOnly": "Yalnızca DM",
|
||||
"allMessages": "Tüm mesajlar",
|
||||
"sendToPublicChannel": "Genel kanala gönderilsin mi?",
|
||||
"selectMarkerTypeAndDestination": "İşaretçi türünü ve hedefi seçin",
|
||||
"noDestinationsAvailableLabel": "Kullanılabilir hedef yok",
|
||||
"image": "Resim",
|
||||
"format": "Biçim",
|
||||
"dimensions": "Boyutlar",
|
||||
"segments": "Segmentler",
|
||||
"transfers": "Transferler",
|
||||
"downloadedBy": "İndiren",
|
||||
"saveDiscoverySettings": "Keşif ayarlarını kaydet",
|
||||
"savePublicInfo": "Genel bilgileri kaydet",
|
||||
"saveRadioSettings": "Radyo ayarlarını kaydet",
|
||||
"savePath": "Yolu kaydet",
|
||||
"wipeDeviceData": "Cihaz verilerini sil",
|
||||
"wipeDevice": "Cihazı sil",
|
||||
"destructiveDeviceActions": "Yıkıcı cihaz işlemleri.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Bir ön ayar seçin veya radyo ayarlarını ince ayarlayın.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Bu cihazın paylaştığı ad ve konumu seçin.",
|
||||
"availableSpaceOnThisDevice": "Bu cihazda kullanılabilir alan.",
|
||||
"used": "Kullanılan",
|
||||
"total": "Toplam",
|
||||
"renameValue": "Değeri yeniden adlandır",
|
||||
"customizeFields": "Alanları özelleştir",
|
||||
"livePreview": "Canlı önizleme",
|
||||
"refreshSchedule": "Yenileme programı",
|
||||
"noResponse": "Yanıt yok",
|
||||
"refreshing": "Yenileniyor",
|
||||
"unavailable": "Kullanılamaz",
|
||||
"pickARelayOrNodeToWatchInSensors": "İzlemek için bir röle veya düğüm seçin.",
|
||||
"publicKeyLabel": "Genel anahtar",
|
||||
"alreadyInContacts": "Zaten kişilerde",
|
||||
"connectToADeviceBeforeAddingContacts": "Kişi eklemeden önce bir cihaza bağlanın",
|
||||
"fromContacts": "Kişilerden",
|
||||
"onlineOnly": "Yalnızca çevrimiçi",
|
||||
"inBoth": "Her ikisinde",
|
||||
"source": "Kaynak",
|
||||
"manualRouteEdit": "Manuel rota düzenle",
|
||||
"observedMeshRoute": "Gözlemlenen mesh rotası",
|
||||
"allMessagesCleared": "Tüm mesajlar temizlendi",
|
||||
"onlineTraceDatabaseCleared": "Çevrimiçi iz veritabanı temizlendi",
|
||||
"packetLogsCleared": "Paket günlükleri temizlendi",
|
||||
"hexDataCopiedToClipboard": "Hex verisi panoya kopyalandı",
|
||||
"developerModeEnabled": "Geliştirici modu etkinleştirildi",
|
||||
"developerModeDisabled": "Geliştirici modu devre dışı bırakıldı",
|
||||
"clipboardIsEmpty": "Pano boş",
|
||||
"contactImported": "Kişi içe aktarıldı",
|
||||
"contactLinkCopiedToClipboard": "Kişi bağlantısı panoya kopyalandı",
|
||||
"failedToExportContact": "Kişi dışa aktarılamadı",
|
||||
"noLogsToExport": "Dışa aktarılacak günlük yok",
|
||||
"exportAsCsv": "CSV olarak dışa aktar",
|
||||
"exportAsText": "Metin olarak dışa aktar",
|
||||
"receivedRfc3339": "Alındı (RFC3339)",
|
||||
"buildTime": "Derleme zamanı",
|
||||
"downloadUrlNotAvailable": "İndirme URL'si mevcut değil",
|
||||
"cannotOpenDownloadUrl": "İndirme URL'si açılamıyor",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Güncelleme kontrolü yalnızca Android'de kullanılabilir",
|
||||
"youAreRunningTheLatestVersion": "En son sürümü kullanıyorsunuz",
|
||||
"updateAvailableButDownloadUrlNotFound": "Güncelleme mevcut ancak indirme URL'si bulunamadı",
|
||||
"startTictactoe": "Tic-Tac-Toe başlat",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe kullanılamıyor",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: rakip bilinmiyor",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: başlangıç bekleniyor",
|
||||
"acceptsShareLinks": "Paylaşım bağlantılarını kabul eder",
|
||||
"supportsRawHex": "Ham hex destekler",
|
||||
"clipboardfriendly": "Pano dostu",
|
||||
"captured": "Yakalandı",
|
||||
"size": "Boyut",
|
||||
"noCustomChannelsToClear": "Temizlenecek özel kanal yok.",
|
||||
"noDeviceContactsToClear": "Temizlenecek cihaz kişisi yok."
|
||||
}
|
||||
|
||||
@@ -2276,5 +2276,304 @@
|
||||
"voiceUnavailable": "Голос зараз недоступний",
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "Запит голосу",
|
||||
"@requestingVoice": {}
|
||||
"@requestingVoice": {},
|
||||
"device": "пристрій",
|
||||
"change": "Змінити",
|
||||
"wizardOverviewDescription": "Ця програма поєднує повідомлення MeshCore, польові оновлення SAR, карти та інструменти пристрою в одному місці.",
|
||||
"wizardOverviewFeature1": "Надсилайте особисті повідомлення, повідомлення кімнат і повідомлення каналів з основної вкладки «Повідомлення».",
|
||||
"wizardOverviewFeature2": "Діліться SAR-маркерами, малюнками на карті, голосовими кліпами та зображеннями через mesh-мережу.",
|
||||
"wizardOverviewFeature3": "Підключайтеся через BLE або TCP, а потім керуйте супутнім радіопристроєм прямо з програми.",
|
||||
"wizardMessagingTitle": "Повідомлення та польові звіти",
|
||||
"wizardMessagingDescription": "Повідомлення тут це більше, ніж просто текст. Програма вже підтримує кілька типів операційних даних і сценаріїв передачі.",
|
||||
"wizardMessagingFeature1": "Надсилайте особисті повідомлення, повідомлення кімнат і трафік каналів з одного редактора.",
|
||||
"wizardMessagingFeature2": "Створюйте оновлення SAR і багаторазові шаблони SAR для типових польових звітів.",
|
||||
"wizardMessagingFeature3": "Передавайте голосові сесії та зображення з індикатором прогресу й оцінками ефірного часу в інтерфейсі.",
|
||||
"wizardConnectDeviceTitle": "Підключити пристрій",
|
||||
"wizardConnectDeviceDescription": "Підключіть своє радіо MeshCore, виберіть назву та застосуйте радіопрофіль перед продовженням.",
|
||||
"wizardSetupBadge": "Налаштування",
|
||||
"wizardOverviewBadge": "Огляд",
|
||||
"wizardConnectedToDevice": "Підключено до {deviceName}",
|
||||
"wizardNoDeviceConnected": "Ще немає підключеного пристрою",
|
||||
"wizardSkipForNow": "Пропустити поки що",
|
||||
"wizardDeviceNameLabel": "Назва пристрою",
|
||||
"wizardDeviceNameHelp": "Ця назва оголошується іншим користувачам MeshCore.",
|
||||
"wizardConfigRegionLabel": "Регіон конфігурації",
|
||||
"wizardConfigRegionHelp": "Використовується повний офіційний список профілів MeshCore. Типово вибрано EU/UK (Narrow).",
|
||||
"wizardPresetNote1": "Переконайтеся, що вибраний профіль відповідає місцевим радіоправилам.",
|
||||
"wizardPresetNote2": "Список відповідає офіційному потоку профілів інструмента MeshCore config.",
|
||||
"wizardPresetNote3": "Для онбордингу типовим залишається EU/UK (Narrow).",
|
||||
"wizardSaving": "Збереження...",
|
||||
"wizardSaveAndContinue": "Зберегти й продовжити",
|
||||
"wizardEnterDeviceName": "Введіть назву пристрою перед продовженням.",
|
||||
"wizardDeviceSetupSaved": "Збережено {deviceName} з профілем {presetName}.",
|
||||
"wizardNetworkTitle": "Контакти, кімнати та ретранслятори",
|
||||
"wizardNetworkDescription": "Вкладка «Контакти» організовує мережу, яку ви виявляєте, і маршрути, які вивчаєте з часом.",
|
||||
"wizardNetworkFeature1": "Переглядайте членів команди, ретранслятори, кімнати, канали та оголошення в очікуванні в одному списку.",
|
||||
"wizardNetworkFeature2": "Використовуйте smart ping, вхід до кімнат, вивчені шляхи й інструменти скидання маршрутів, коли зв'язок стає нестабільним.",
|
||||
"wizardNetworkFeature3": "Створюйте канали та керуйте мережевими призначеннями, не виходячи з програми.",
|
||||
"wizardMapOpsTitle": "Мапа, сліди та спільна геометрія",
|
||||
"wizardMapOpsDescription": "Мапа програми напряму пов'язана з повідомленнями, відстеженням і SAR-накладками, а не є окремим переглядачем.",
|
||||
"wizardMapOpsFeature1": "Відстежуйте власну позицію, місця розташування команди та сліди руху на мапі.",
|
||||
"wizardMapOpsFeature2": "Відкривайте малюнки з повідомлень, переглядайте їх в інтерфейсі та видаляйте з мапи за потреби.",
|
||||
"wizardMapOpsFeature3": "Використовуйте мапи ретрансляторів і спільні накладки, щоб розуміти покриття мережі в полі.",
|
||||
"wizardToolsTitle": "Інструменти поза повідомленнями",
|
||||
"wizardToolsDescription": "Тут є більше, ніж чотири основні вкладки. Програма також включає налаштування, діагностику та необов'язкові сценарії датчиків.",
|
||||
"wizardToolsFeature1": "Відкрийте налаштування пристрою, щоб змінити параметри радіо, телеметрію, потужність TX і дані супутнього пристрою.",
|
||||
"wizardToolsFeature2": "Увімкніть вкладку «Датчики», коли потрібні панелі моніторингу та швидкі дії оновлення.",
|
||||
"wizardToolsFeature3": "Використовуйте журнали пакетів, сканування спектра та діагностику розробника для усунення проблем mesh-мережі.",
|
||||
"contactInSensors": "У датчиках",
|
||||
"contactAddToSensors": "Додати до датчиків",
|
||||
"contactSetPath": "Задати шлях",
|
||||
"contactAddedToSensors": "{contactName} додано до Датчиків",
|
||||
"contactFailedToClearRoute": "Не вдалося очистити маршрут: {error}",
|
||||
"contactRouteCleared": "Маршрут очищено",
|
||||
"contactRouteSet": "Маршрут задано: {route}",
|
||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "Тайм-аут ACK",
|
||||
"opcode": "Опкод",
|
||||
"payload": "Корисне навантаження",
|
||||
"hops": "Хопи",
|
||||
"hashSize": "Розмір хешу",
|
||||
"pathBytes": "Байти шляху",
|
||||
"selectedPath": "Обраний шлях",
|
||||
"estimatedTx": "Очікувана передача",
|
||||
"senderToReceipt": "Від відправника до отримання",
|
||||
"receivedCopies": "Отримані копії",
|
||||
"retryCause": "Причина повтору",
|
||||
"retryMode": "Режим повтору",
|
||||
"retryResult": "Результат повтору",
|
||||
"lastRetry": "Остання спроба",
|
||||
"rxPackets": "RX-пакети",
|
||||
"mesh": "Mesh",
|
||||
"rate": "Швидкість",
|
||||
"window": "Вікно",
|
||||
"posttxDelay": "Затримка після передачі",
|
||||
"bandpass": "Смуговий",
|
||||
"bandpassFilterVoice": "Смуговий фільтр голосу",
|
||||
"frequency": "Частота",
|
||||
"australia": "Австралія",
|
||||
"australiaNarrow": "Австралія (Вузький)",
|
||||
"australiaQld": "Австралія: QLD",
|
||||
"australiaSaWa": "Австралія: SA, WA",
|
||||
"newZealand": "Нова Зеландія",
|
||||
"newZealandNarrow": "Нова Зеландія (Вузький)",
|
||||
"switzerland": "Швейцарія",
|
||||
"portugal433": "Португалія 433",
|
||||
"portugal868": "Португалія 868",
|
||||
"czechRepublicNarrow": "Чехія (Вузький)",
|
||||
"eu433mhzLongRange": "ЄС 433МГц (Далекий)",
|
||||
"euukDeprecated": "ЄС/Великобританія (Застаріле)",
|
||||
"euukNarrow": "ЄС/Великобританія (Вузький)",
|
||||
"usacanadaRecommended": "США/Канада (Рекомендовано)",
|
||||
"vietnamDeprecated": "В'єтнам (Застаріле)",
|
||||
"vietnamNarrow": "В'єтнам (Вузький)",
|
||||
"active": "Активно",
|
||||
"addContact": "Додати контакт",
|
||||
"all": "Усі",
|
||||
"autoResolve": "Автоматичне вирішення",
|
||||
"clearAllLabel": "Очистити все",
|
||||
"clearRelays": "Очистити реле",
|
||||
"clearFilters": "Очистити фільтри",
|
||||
"clearRoute": "Очистити маршрут",
|
||||
"clearMessages": "Очистити повідомлення",
|
||||
"clearScale": "Очистити масштаб",
|
||||
"clearDiscoveries": "Очистити виявлення",
|
||||
"clearOnlineTraceDatabase": "Очистити базу трасувань",
|
||||
"clearAllChannels": "Очистити всі канали",
|
||||
"clearAllContacts": "Очистити всі контакти",
|
||||
"clearChannels": "Очистити канали",
|
||||
"clearContacts": "Очистити контакти",
|
||||
"clearPathOnMaxRetry": "Очистити шлях при макс. спробі",
|
||||
"create": "Створити",
|
||||
"custom": "Користувацький",
|
||||
"defaultValue": "За замовчуванням",
|
||||
"duplicate": "Дублювати",
|
||||
"editName": "Редагувати ім'я",
|
||||
"open": "Відкрити",
|
||||
"paste": "Вставити",
|
||||
"preview": "Попередній перегляд",
|
||||
"remove": "Видалити",
|
||||
"rename": "Перейменувати",
|
||||
"resolveAll": "Вирішити все",
|
||||
"send": "Надіслати",
|
||||
"sendAnyway": "Надіслати все одно",
|
||||
"share": "Поділитися",
|
||||
"shareContact": "Поділитися контактом",
|
||||
"trace": "Трасування",
|
||||
"use": "Використати",
|
||||
"useSelectedFrequency": "Використати обрану частоту",
|
||||
"discovery": "Виявлення",
|
||||
"discoverRepeaters": "Виявити повторювачі",
|
||||
"discoverSensors": "Виявити датчики",
|
||||
"repeaterDiscoverySent": "Виявлення повторювачів надіслано",
|
||||
"sensorDiscoverySent": "Виявлення датчиків надіслано",
|
||||
"clearedPendingDiscoveries": "Очікувані виявлення очищено.",
|
||||
"autoDiscovery": "Автоматичне виявлення",
|
||||
"enableAutomaticAdding": "Увімкнути автоматичне додавання",
|
||||
"autoaddRepeaters": "Автододавання повторювачів",
|
||||
"autoaddRoomServers": "Автододавання серверів кімнат",
|
||||
"autoaddSensors": "Автододавання датчиків",
|
||||
"autoaddUsers": "Автододавання користувачів",
|
||||
"overwriteOldestWhenFull": "Перезаписати найстаріші при заповненні",
|
||||
"storage": "Сховище",
|
||||
"dangerZone": "Небезпечна зона",
|
||||
"profiles": "Профілі",
|
||||
"favourites": "Обране",
|
||||
"sensors": "Датчики",
|
||||
"others": "Інші",
|
||||
"gpsModule": "Модуль GPS",
|
||||
"liveTraffic": "Живий трафік",
|
||||
"repeatersMap": "Карта повторювачів",
|
||||
"spectrumScan": "Сканування спектру",
|
||||
"blePacketLogs": "Журнали BLE-пакетів",
|
||||
"onlineTraceDatabase": "База трасувань",
|
||||
"routePathByteSize": "Розмір шляху в байтах",
|
||||
"messageNotifications": "Сповіщення про повідомлення",
|
||||
"sarAlerts": "SAR-сповіщення",
|
||||
"discoveryNotifications": "Сповіщення про виявлення",
|
||||
"updateNotifications": "Сповіщення про оновлення",
|
||||
"muteWhileAppIsOpen": "Без звуку при відкритому додатку",
|
||||
"disableContacts": "Вимкнути контакти",
|
||||
"enableSensorsTab": "Увімкнути вкладку Датчики",
|
||||
"enableProfiles": "Увімкнути профілі",
|
||||
"autoRouteRotation": "Автоматична ротація маршруту",
|
||||
"nearestRepeaterFallback": "Найближчий повторювач як резерв",
|
||||
"deleteAllStoredMessageHistory": "Видалити всю історію повідомлень",
|
||||
"messageFontSize": "Розмір шрифту повідомлень",
|
||||
"rotateMapWithHeading": "Обертати карту за напрямком",
|
||||
"showMapDebugInfo": "Показати налагоджувальну інфо карти",
|
||||
"openMapInFullscreen": "Відкрити карту на повний екран",
|
||||
"showSarMarkersLabel": "Показати SAR-маркери",
|
||||
"displaySarMarkersOnTheMainMap": "Показувати SAR-маркери на головній карті",
|
||||
"showAllContactTrailsLabel": "Показати всі сліди контактів",
|
||||
"hideRepeatersOnMap": "Сховати повторювачі на карті",
|
||||
"setMapScale": "Встановити масштаб карти",
|
||||
"customMapScaleSaved": "Користувацький масштаб карти збережено",
|
||||
"voiceBitrate": "Бітрейт голосу",
|
||||
"voiceCompressor": "Компресор голосу",
|
||||
"balancesQuietAndLoudSpeechLevels": "Вирівнює тихий і гучний рівні мови",
|
||||
"voiceLimiter": "Лімітер голосу",
|
||||
"preventsClippingPeaksBeforeEncoding": "Запобігає обрізанню піків перед кодуванням",
|
||||
"micAutoGain": "Автопідсилення мікрофона",
|
||||
"letsTheRecorderAdjustInputLevel": "Дозволяє рекордеру налаштувати рівень входу",
|
||||
"echoCancellation": "Придушення ехо",
|
||||
"noiseSuppression": "Придушення шуму",
|
||||
"trimSilenceInVoiceMessages": "Обрізати тишу в голосових повідомленнях",
|
||||
"compressor": "Компресор",
|
||||
"limiter": "Лімітер",
|
||||
"autoGain": "Автопідсилення",
|
||||
"echoCancel": "Ехо",
|
||||
"noiseSuppress": "Шум",
|
||||
"silenceTrim": "Тиша",
|
||||
"maxImageSize": "Максимальний розмір зображення",
|
||||
"imageCompression": "Стиснення зображення",
|
||||
"grayscale": "Відтінки сірого",
|
||||
"ultraMode": "Ультра режим",
|
||||
"fastPrivateGpsUpdates": "Швидкі приватні GPS-оновлення",
|
||||
"movementThreshold": "Поріг руху",
|
||||
"fastGpsMovementThreshold": "Поріг руху швидкого GPS",
|
||||
"fastGpsActiveuseInterval": "Інтервал активного використання швидкого GPS",
|
||||
"activeuseUpdateInterval": "Інтервал оновлення при активному використанні",
|
||||
"repeatNearbyTraffic": "Повторювати близький трафік",
|
||||
"relayThroughRepeatersAcrossTheMesh": "Ретранслювати через повторювачі по мережі",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "Лише поблизу, без flood повторювачів",
|
||||
"multihop": "Багатохоповий",
|
||||
"createProfile": "Створити профіль",
|
||||
"renameProfile": "Перейменувати профіль",
|
||||
"newProfile": "Новий профіль",
|
||||
"manageProfiles": "Керувати профілями",
|
||||
"enableProfilesToStartManagingThem": "Увімкніть профілі, щоб почати керувати ними.",
|
||||
"openMessage": "Відкрити повідомлення",
|
||||
"jumpToTheRelatedSarMessage": "Перейти до пов'язаного SAR-повідомлення",
|
||||
"removeSarMarker": "Видалити SAR-маркер",
|
||||
"pleaseSelectADestinationToSendSarMarker": "Виберіть призначення для надсилання SAR-маркера",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR-маркер надіслано на публічний канал",
|
||||
"sarMarkerSentToRoom": "SAR-маркер надіслано в кімнату",
|
||||
"loadFromGallery": "Завантажити з галереї",
|
||||
"replaceImage": "Замінити зображення",
|
||||
"selectFromGallery": "Вибрати з галереї",
|
||||
"team": "Команда",
|
||||
"found": "Знайдено",
|
||||
"staging": "Місце збору",
|
||||
"object": "Об'єкт",
|
||||
"quiet": "Тихо",
|
||||
"moderate": "Помірно",
|
||||
"busy": "Зайнято",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "Сканування спектру не знайшло частот-кандидатів",
|
||||
"searchMessages": "Пошук повідомлень",
|
||||
"sendImageFromGallery": "Надіслати зображення з галереї",
|
||||
"takePhoto": "Зробити фото",
|
||||
"dmOnly": "Лише особисті повідомлення",
|
||||
"allMessages": "Усі повідомлення",
|
||||
"sendToPublicChannel": "Надіслати в публічний канал?",
|
||||
"selectMarkerTypeAndDestination": "Виберіть тип маркера та призначення",
|
||||
"noDestinationsAvailableLabel": "Немає доступних призначень",
|
||||
"image": "Зображення",
|
||||
"format": "Формат",
|
||||
"dimensions": "Розміри",
|
||||
"segments": "Сегменти",
|
||||
"transfers": "Передачі",
|
||||
"downloadedBy": "Завантажено",
|
||||
"saveDiscoverySettings": "Зберегти налаштування виявлення",
|
||||
"savePublicInfo": "Зберегти публічну інформацію",
|
||||
"saveRadioSettings": "Зберегти налаштування радіо",
|
||||
"savePath": "Зберегти шлях",
|
||||
"wipeDeviceData": "Стерти дані пристрою",
|
||||
"wipeDevice": "Стерти пристрій",
|
||||
"destructiveDeviceActions": "Деструктивні дії пристрою.",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "Оберіть пресет або налаштуйте радіо вручну.",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "Оберіть ім'я та місцеположення, якими ділиться пристрій.",
|
||||
"availableSpaceOnThisDevice": "Доступний простір на цьому пристрої.",
|
||||
"used": "Використано",
|
||||
"total": "Загалом",
|
||||
"renameValue": "Перейменувати значення",
|
||||
"customizeFields": "Налаштувати поля",
|
||||
"livePreview": "Попередній перегляд наживо",
|
||||
"refreshSchedule": "Розклад оновлення",
|
||||
"noResponse": "Немає відповіді",
|
||||
"refreshing": "Оновлення",
|
||||
"unavailable": "Недоступно",
|
||||
"pickARelayOrNodeToWatchInSensors": "Оберіть реле або вузол для спостереження.",
|
||||
"publicKeyLabel": "Публічний ключ",
|
||||
"alreadyInContacts": "Вже в контактах",
|
||||
"connectToADeviceBeforeAddingContacts": "Підключіться до пристрою перед додаванням контактів",
|
||||
"fromContacts": "З контактів",
|
||||
"onlineOnly": "Лише онлайн",
|
||||
"inBoth": "В обох",
|
||||
"source": "Джерело",
|
||||
"manualRouteEdit": "Ручне редагування маршруту",
|
||||
"observedMeshRoute": "Спостережуваний mesh-маршрут",
|
||||
"allMessagesCleared": "Усі повідомлення очищено",
|
||||
"onlineTraceDatabaseCleared": "Базу трасувань очищено",
|
||||
"packetLogsCleared": "Журнали пакетів очищено",
|
||||
"hexDataCopiedToClipboard": "Hex-дані скопійовано в буфер",
|
||||
"developerModeEnabled": "Режим розробника увімкнено",
|
||||
"developerModeDisabled": "Режим розробника вимкнено",
|
||||
"clipboardIsEmpty": "Буфер обміну порожній",
|
||||
"contactImported": "Контакт імпортовано",
|
||||
"contactLinkCopiedToClipboard": "Посилання на контакт скопійовано в буфер",
|
||||
"failedToExportContact": "Не вдалося експортувати контакт",
|
||||
"noLogsToExport": "Немає журналів для експорту",
|
||||
"exportAsCsv": "Експортувати як CSV",
|
||||
"exportAsText": "Експортувати як текст",
|
||||
"receivedRfc3339": "Отримано (RFC3339)",
|
||||
"buildTime": "Час збірки",
|
||||
"downloadUrlNotAvailable": "URL завантаження недоступний",
|
||||
"cannotOpenDownloadUrl": "Не вдається відкрити URL завантаження",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "Перевірка оновлень доступна лише на Android",
|
||||
"youAreRunningTheLatestVersion": "Ви використовуєте останню версію",
|
||||
"updateAvailableButDownloadUrlNotFound": "Оновлення доступне, але URL завантаження не знайдено",
|
||||
"startTictactoe": "Почати Tic-Tac-Toe",
|
||||
"tictactoeUnavailable": "Tic-Tac-Toe недоступно",
|
||||
"tictactoeOpponentUnknown": "Tic-Tac-Toe: суперник невідомий",
|
||||
"tictactoeWaitingForStart": "Tic-Tac-Toe: очікування початку",
|
||||
"acceptsShareLinks": "Приймає посилання для обміну",
|
||||
"supportsRawHex": "Підтримує raw hex",
|
||||
"clipboardfriendly": "Зручно для буфера",
|
||||
"captured": "Захоплено",
|
||||
"size": "Розмір",
|
||||
"noCustomChannelsToClear": "Немає користувацьких каналів для очищення.",
|
||||
"noDeviceContactsToClear": "Немає контактів пристрою для очищення."
|
||||
}
|
||||
|
||||
@@ -2228,78 +2228,352 @@
|
||||
"description": "没有可发送的聊天联系人时的消息"
|
||||
},
|
||||
"reply": "回复",
|
||||
"@reply": {
|
||||
},
|
||||
"@reply": {},
|
||||
"technicalDetails": "技术细节",
|
||||
"@technicalDetails": {
|
||||
},
|
||||
"@technicalDetails": {},
|
||||
"messageTechnicalDetails": "消息技术细节",
|
||||
"@messageTechnicalDetails": {
|
||||
},
|
||||
"@messageTechnicalDetails": {},
|
||||
"linkQuality": "链路质量",
|
||||
"@linkQuality": {
|
||||
},
|
||||
"@linkQuality": {},
|
||||
"delivery": "投递",
|
||||
"@delivery": {
|
||||
},
|
||||
"@delivery": {},
|
||||
"status": "状态",
|
||||
"@status": {
|
||||
},
|
||||
"@status": {},
|
||||
"expectedAckTag": "预期的 ACK 标签",
|
||||
"@expectedAckTag": {
|
||||
},
|
||||
"@expectedAckTag": {},
|
||||
"roundTrip": "往返时间",
|
||||
"@roundTrip": {
|
||||
},
|
||||
"@roundTrip": {},
|
||||
"retryAttempt": "重试尝试",
|
||||
"@retryAttempt": {
|
||||
},
|
||||
"@retryAttempt": {},
|
||||
"floodFallback": "泛洪回退",
|
||||
"@floodFallback": {
|
||||
},
|
||||
"@floodFallback": {},
|
||||
"identity": "身份",
|
||||
"@identity": {
|
||||
},
|
||||
"@identity": {},
|
||||
"messageId": "消息 ID",
|
||||
"@messageId": {
|
||||
},
|
||||
"@messageId": {},
|
||||
"sender": "发送者",
|
||||
"@sender": {
|
||||
},
|
||||
"@sender": {},
|
||||
"senderKey": "发送者密钥",
|
||||
"@senderKey": {
|
||||
},
|
||||
"@senderKey": {},
|
||||
"recipient": "接收者",
|
||||
"@recipient": {
|
||||
},
|
||||
"@recipient": {},
|
||||
"recipientKey": "接收者密钥",
|
||||
"@recipientKey": {
|
||||
},
|
||||
"@recipientKey": {},
|
||||
"voice": "语音",
|
||||
"@voice": {
|
||||
},
|
||||
"@voice": {},
|
||||
"voiceId": "语音 ID",
|
||||
"@voiceId": {
|
||||
},
|
||||
"@voiceId": {},
|
||||
"envelope": "信封",
|
||||
"@envelope": {
|
||||
},
|
||||
"@envelope": {},
|
||||
"sessionProgress": "会话进度",
|
||||
"@sessionProgress": {
|
||||
},
|
||||
"@sessionProgress": {},
|
||||
"complete": "完成",
|
||||
"@complete": {
|
||||
},
|
||||
"@complete": {},
|
||||
"rawDump": "原始数据",
|
||||
"@rawDump": {
|
||||
},
|
||||
"@rawDump": {},
|
||||
"cannotRetryMissingRecipient": "无法重试:缺少接收者信息",
|
||||
"@cannotRetryMissingRecipient": {
|
||||
},
|
||||
"@cannotRetryMissingRecipient": {},
|
||||
"voiceUnavailable": "当前语音不可用",
|
||||
"@voiceUnavailable": {
|
||||
},
|
||||
"@voiceUnavailable": {},
|
||||
"requestingVoice": "正在请求语音",
|
||||
"@requestingVoice": {
|
||||
}
|
||||
"@requestingVoice": {},
|
||||
"device": "设备",
|
||||
"change": "更改",
|
||||
"wizardOverviewDescription": "此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。",
|
||||
"wizardOverviewFeature1": "在主消息标签页中发送私聊消息、房间消息和频道消息。",
|
||||
"wizardOverviewFeature2": "通过 mesh 网络共享 SAR 标记、地图绘图、语音片段和图像。",
|
||||
"wizardOverviewFeature3": "通过 BLE 或 TCP 连接,然后直接在应用中管理配套电台。",
|
||||
"wizardMessagingTitle": "消息与现场报告",
|
||||
"wizardMessagingDescription": "这里的消息不只是纯文本。应用已经支持多种操作载荷和传输流程。",
|
||||
"wizardMessagingFeature1": "通过一个编辑器发送私聊消息、房间消息和频道流量。",
|
||||
"wizardMessagingFeature2": "为常见现场报告创建 SAR 更新和可复用的 SAR 模板。",
|
||||
"wizardMessagingFeature3": "传输语音会话和图像,并在界面中显示进度和空口时间估算。",
|
||||
"wizardConnectDeviceTitle": "连接设备",
|
||||
"wizardConnectDeviceDescription": "连接你的 MeshCore 电台,选择名称,并在继续前应用无线电预设。",
|
||||
"wizardSetupBadge": "设置",
|
||||
"wizardOverviewBadge": "概览",
|
||||
"wizardConnectedToDevice": "已连接到 {deviceName}",
|
||||
"wizardNoDeviceConnected": "尚未连接设备",
|
||||
"wizardSkipForNow": "暂时跳过",
|
||||
"wizardDeviceNameLabel": "设备名称",
|
||||
"wizardDeviceNameHelp": "此名称会广播给其他 MeshCore 用户。",
|
||||
"wizardConfigRegionLabel": "配置区域",
|
||||
"wizardConfigRegionHelp": "使用完整的官方 MeshCore 预设列表。默认值为 EU/UK (Narrow)。",
|
||||
"wizardPresetNote1": "请确认所选预设符合你所在地区的无线电规定。",
|
||||
"wizardPresetNote2": "该列表与 MeshCore 配置工具的官方预设源保持一致。",
|
||||
"wizardPresetNote3": "在引导设置中默认保持选中 EU/UK (Narrow)。",
|
||||
"wizardSaving": "正在保存...",
|
||||
"wizardSaveAndContinue": "保存并继续",
|
||||
"wizardEnterDeviceName": "继续前请输入设备名称。",
|
||||
"wizardDeviceSetupSaved": "已保存 {deviceName},配置为 {presetName}。",
|
||||
"wizardNetworkTitle": "联系人、房间和中继器",
|
||||
"wizardNetworkDescription": "联系人标签页会整理你发现的网络以及随着时间学习到的路由。",
|
||||
"wizardNetworkFeature1": "在一个列表中查看团队成员、中继器、房间、频道和待处理广播。",
|
||||
"wizardNetworkFeature2": "当连接变得混乱时,使用智能 ping、房间登录、已学习路径和路由重置工具。",
|
||||
"wizardNetworkFeature3": "无需离开应用即可创建频道并管理网络目的地。",
|
||||
"wizardMapOpsTitle": "地图、轨迹与共享几何",
|
||||
"wizardMapOpsDescription": "应用地图直接与消息、跟踪和 SAR 叠加层关联,而不是单独的查看器。",
|
||||
"wizardMapOpsFeature1": "在地图上跟踪你自己的位置、队友位置和移动轨迹。",
|
||||
"wizardMapOpsFeature2": "打开消息中的绘图,内联预览,并在需要时将其从地图上移除。",
|
||||
"wizardMapOpsFeature3": "使用中继器地图视图和共享叠加层来了解现场网络覆盖范围。",
|
||||
"wizardToolsTitle": "消息之外的工具",
|
||||
"wizardToolsDescription": "这里不只有四个主标签页。应用还包含配置、诊断和可选的传感器工作流。",
|
||||
"wizardToolsFeature1": "打开设备配置以更改无线电设置、遥测、TX 功率和配套设备详情。",
|
||||
"wizardToolsFeature2": "当你需要受监控的传感器面板和快速刷新操作时,启用传感器标签页。",
|
||||
"wizardToolsFeature3": "在排查 mesh 网络问题时使用数据包日志、频谱扫描和开发者诊断。",
|
||||
"contactInSensors": "在传感器中",
|
||||
"contactAddToSensors": "添加到传感器",
|
||||
"contactSetPath": "设置路径",
|
||||
"contactAddedToSensors": "已将 {contactName} 添加到传感器",
|
||||
"contactFailedToClearRoute": "清除路由失败:{error}",
|
||||
"contactRouteCleared": "路由已清除",
|
||||
"contactRouteSet": "路由已设置:{route}",
|
||||
"contactFailedToSetRoute": "设置路由失败:{error}",
|
||||
"rssi": "RSSI",
|
||||
"snr": "SNR",
|
||||
"ackTimeout": "ACK 超时",
|
||||
"opcode": "操作码",
|
||||
"payload": "有效载荷",
|
||||
"hops": "跳数",
|
||||
"hashSize": "哈希大小",
|
||||
"pathBytes": "路径字节",
|
||||
"selectedPath": "选定路径",
|
||||
"estimatedTx": "预计传输",
|
||||
"senderToReceipt": "发送到接收",
|
||||
"receivedCopies": "收到的副本",
|
||||
"retryCause": "重试原因",
|
||||
"retryMode": "重试模式",
|
||||
"retryResult": "重试结果",
|
||||
"lastRetry": "最后重试",
|
||||
"rxPackets": "RX 数据包",
|
||||
"mesh": "Mesh",
|
||||
"rate": "速率",
|
||||
"window": "窗口",
|
||||
"posttxDelay": "发送后延迟",
|
||||
"bandpass": "带通",
|
||||
"bandpassFilterVoice": "带通语音滤波",
|
||||
"frequency": "频率",
|
||||
"australia": "澳大利亚",
|
||||
"australiaNarrow": "澳大利亚(窄带)",
|
||||
"australiaQld": "澳大利亚:QLD",
|
||||
"australiaSaWa": "澳大利亚:SA, WA",
|
||||
"newZealand": "新西兰",
|
||||
"newZealandNarrow": "新西兰(窄带)",
|
||||
"switzerland": "瑞士",
|
||||
"portugal433": "葡萄牙 433",
|
||||
"portugal868": "葡萄牙 868",
|
||||
"czechRepublicNarrow": "捷克(窄带)",
|
||||
"eu433mhzLongRange": "欧盟 433MHz(远距离)",
|
||||
"euukDeprecated": "欧盟/英国(已弃用)",
|
||||
"euukNarrow": "欧盟/英国(窄带)",
|
||||
"usacanadaRecommended": "美国/加拿大(推荐)",
|
||||
"vietnamDeprecated": "越南(已弃用)",
|
||||
"vietnamNarrow": "越南(窄带)",
|
||||
"active": "活动",
|
||||
"addContact": "添加联系人",
|
||||
"all": "全部",
|
||||
"autoResolve": "自动解析",
|
||||
"clearAllLabel": "清除全部",
|
||||
"clearRelays": "清除中继",
|
||||
"clearFilters": "清除过滤器",
|
||||
"clearRoute": "清除路由",
|
||||
"clearMessages": "清除消息",
|
||||
"clearScale": "清除比例",
|
||||
"clearDiscoveries": "清除发现",
|
||||
"clearOnlineTraceDatabase": "清除在线追踪数据库",
|
||||
"clearAllChannels": "清除所有频道",
|
||||
"clearAllContacts": "清除所有联系人",
|
||||
"clearChannels": "清除频道",
|
||||
"clearContacts": "清除联系人",
|
||||
"clearPathOnMaxRetry": "最大重试时清除路径",
|
||||
"create": "创建",
|
||||
"custom": "自定义",
|
||||
"defaultValue": "默认",
|
||||
"duplicate": "复制",
|
||||
"editName": "编辑名称",
|
||||
"open": "打开",
|
||||
"paste": "粘贴",
|
||||
"preview": "预览",
|
||||
"remove": "移除",
|
||||
"rename": "重命名",
|
||||
"resolveAll": "解析全部",
|
||||
"send": "发送",
|
||||
"sendAnyway": "仍然发送",
|
||||
"share": "分享",
|
||||
"shareContact": "分享联系人",
|
||||
"trace": "追踪",
|
||||
"use": "使用",
|
||||
"useSelectedFrequency": "使用选定频率",
|
||||
"discovery": "发现",
|
||||
"discoverRepeaters": "发现中继器",
|
||||
"discoverSensors": "发现传感器",
|
||||
"repeaterDiscoverySent": "中继器发现已发送",
|
||||
"sensorDiscoverySent": "传感器发现已发送",
|
||||
"clearedPendingDiscoveries": "已清除待处理的发现。",
|
||||
"autoDiscovery": "自动发现",
|
||||
"enableAutomaticAdding": "启用自动添加",
|
||||
"autoaddRepeaters": "自动添加中继器",
|
||||
"autoaddRoomServers": "自动添加房间服务器",
|
||||
"autoaddSensors": "自动添加传感器",
|
||||
"autoaddUsers": "自动添加用户",
|
||||
"overwriteOldestWhenFull": "满时覆盖最旧的",
|
||||
"storage": "存储",
|
||||
"dangerZone": "危险区域",
|
||||
"profiles": "配置文件",
|
||||
"favourites": "收藏夹",
|
||||
"sensors": "传感器",
|
||||
"others": "其他",
|
||||
"gpsModule": "GPS 模块",
|
||||
"liveTraffic": "实时流量",
|
||||
"repeatersMap": "中继器地图",
|
||||
"spectrumScan": "频谱扫描",
|
||||
"blePacketLogs": "BLE 数据包日志",
|
||||
"onlineTraceDatabase": "在线追踪数据库",
|
||||
"routePathByteSize": "路由路径字节大小",
|
||||
"messageNotifications": "消息通知",
|
||||
"sarAlerts": "SAR 警报",
|
||||
"discoveryNotifications": "发现通知",
|
||||
"updateNotifications": "更新通知",
|
||||
"muteWhileAppIsOpen": "应用打开时静音",
|
||||
"disableContacts": "禁用联系人",
|
||||
"enableSensorsTab": "启用传感器标签",
|
||||
"enableProfiles": "启用配置文件",
|
||||
"autoRouteRotation": "自动路由轮换",
|
||||
"nearestRepeaterFallback": "最近中继器回退",
|
||||
"deleteAllStoredMessageHistory": "删除所有存储的消息历史",
|
||||
"messageFontSize": "消息字体大小",
|
||||
"rotateMapWithHeading": "随航向旋转地图",
|
||||
"showMapDebugInfo": "显示地图调试信息",
|
||||
"openMapInFullscreen": "全屏打开地图",
|
||||
"showSarMarkersLabel": "显示 SAR 标记",
|
||||
"displaySarMarkersOnTheMainMap": "在主地图上显示 SAR 标记",
|
||||
"showAllContactTrailsLabel": "显示所有联系人轨迹",
|
||||
"hideRepeatersOnMap": "在地图上隐藏中继器",
|
||||
"setMapScale": "设置地图比例",
|
||||
"customMapScaleSaved": "自定义地图比例已保存",
|
||||
"voiceBitrate": "语音比特率",
|
||||
"voiceCompressor": "语音压缩器",
|
||||
"balancesQuietAndLoudSpeechLevels": "平衡安静和响亮的语音级别",
|
||||
"voiceLimiter": "语音限制器",
|
||||
"preventsClippingPeaksBeforeEncoding": "防止编码前的削波",
|
||||
"micAutoGain": "麦克风自动增益",
|
||||
"letsTheRecorderAdjustInputLevel": "让录音器调整输入级别",
|
||||
"echoCancellation": "回声消除",
|
||||
"noiseSuppression": "噪音抑制",
|
||||
"trimSilenceInVoiceMessages": "修剪语音消息中的静音",
|
||||
"compressor": "压缩器",
|
||||
"limiter": "限制器",
|
||||
"autoGain": "自动增益",
|
||||
"echoCancel": "回声",
|
||||
"noiseSuppress": "噪声",
|
||||
"silenceTrim": "静音",
|
||||
"maxImageSize": "最大图片大小",
|
||||
"imageCompression": "图片压缩",
|
||||
"grayscale": "灰度",
|
||||
"ultraMode": "超级模式",
|
||||
"fastPrivateGpsUpdates": "快速私有 GPS 更新",
|
||||
"movementThreshold": "移动阈值",
|
||||
"fastGpsMovementThreshold": "快速 GPS 移动阈值",
|
||||
"fastGpsActiveuseInterval": "快速 GPS 活动使用间隔",
|
||||
"activeuseUpdateInterval": "活动使用更新间隔",
|
||||
"repeatNearbyTraffic": "转发附近流量",
|
||||
"relayThroughRepeatersAcrossTheMesh": "通过网格中的中继器转发",
|
||||
"nearbyOnlyWithoutRepeaterFlooding": "仅附近,无中继器泛洪",
|
||||
"multihop": "多跳",
|
||||
"createProfile": "创建配置文件",
|
||||
"renameProfile": "重命名配置文件",
|
||||
"newProfile": "新配置文件",
|
||||
"manageProfiles": "管理配置文件",
|
||||
"enableProfilesToStartManagingThem": "启用配置文件以开始管理它们。",
|
||||
"openMessage": "打开消息",
|
||||
"jumpToTheRelatedSarMessage": "跳转到相关的 SAR 消息",
|
||||
"removeSarMarker": "移除 SAR 标记",
|
||||
"pleaseSelectADestinationToSendSarMarker": "请选择目标以发送 SAR 标记",
|
||||
"sarMarkerBroadcastToPublicChannel": "SAR 标记已广播到公共频道",
|
||||
"sarMarkerSentToRoom": "SAR 标记已发送到房间",
|
||||
"loadFromGallery": "从图库加载",
|
||||
"replaceImage": "替换图片",
|
||||
"selectFromGallery": "从图库选择",
|
||||
"team": "团队",
|
||||
"found": "已找到",
|
||||
"staging": "集结区",
|
||||
"object": "物体",
|
||||
"quiet": "安静",
|
||||
"moderate": "中等",
|
||||
"busy": "繁忙",
|
||||
"spectrumScanReturnedNoCandidateFrequencies": "频谱扫描未找到候选频率",
|
||||
"searchMessages": "搜索消息",
|
||||
"sendImageFromGallery": "从图库发送图片",
|
||||
"takePhoto": "拍照",
|
||||
"dmOnly": "仅私信",
|
||||
"allMessages": "所有消息",
|
||||
"sendToPublicChannel": "发送到公共频道?",
|
||||
"selectMarkerTypeAndDestination": "选择标记类型和目标",
|
||||
"noDestinationsAvailableLabel": "没有可用的目标",
|
||||
"image": "图片",
|
||||
"format": "格式",
|
||||
"dimensions": "尺寸",
|
||||
"segments": "分段",
|
||||
"transfers": "传输",
|
||||
"downloadedBy": "已下载",
|
||||
"saveDiscoverySettings": "保存发现设置",
|
||||
"savePublicInfo": "保存公共信息",
|
||||
"saveRadioSettings": "保存无线电设置",
|
||||
"savePath": "保存路径",
|
||||
"wipeDeviceData": "擦除设备数据",
|
||||
"wipeDevice": "擦除设备",
|
||||
"destructiveDeviceActions": "破坏性设备操作。",
|
||||
"chooseAPresetOrFinetuneCustomRadioSettings": "选择预设或微调自定义无线电设置。",
|
||||
"chooseTheNameAndLocationThisDeviceShares": "选择此设备共享的名称和位置。",
|
||||
"availableSpaceOnThisDevice": "此设备上的可用空间。",
|
||||
"used": "已使用",
|
||||
"total": "总计",
|
||||
"renameValue": "重命名值",
|
||||
"customizeFields": "自定义字段",
|
||||
"livePreview": "实时预览",
|
||||
"refreshSchedule": "刷新计划",
|
||||
"noResponse": "无响应",
|
||||
"refreshing": "刷新中",
|
||||
"unavailable": "不可用",
|
||||
"pickARelayOrNodeToWatchInSensors": "选择要在传感器中监视的中继或节点。",
|
||||
"publicKeyLabel": "公钥",
|
||||
"alreadyInContacts": "已在联系人中",
|
||||
"connectToADeviceBeforeAddingContacts": "添加联系人前请连接到设备",
|
||||
"fromContacts": "来自联系人",
|
||||
"onlineOnly": "仅在线",
|
||||
"inBoth": "两者中",
|
||||
"source": "来源",
|
||||
"manualRouteEdit": "手动路由编辑",
|
||||
"observedMeshRoute": "观察到的网格路由",
|
||||
"allMessagesCleared": "所有消息已清除",
|
||||
"onlineTraceDatabaseCleared": "在线追踪数据库已清除",
|
||||
"packetLogsCleared": "数据包日志已清除",
|
||||
"hexDataCopiedToClipboard": "十六进制数据已复制到剪贴板",
|
||||
"developerModeEnabled": "开发者模式已启用",
|
||||
"developerModeDisabled": "开发者模式已禁用",
|
||||
"clipboardIsEmpty": "剪贴板为空",
|
||||
"contactImported": "联系人已导入",
|
||||
"contactLinkCopiedToClipboard": "联系人链接已复制到剪贴板",
|
||||
"failedToExportContact": "导出联系人失败",
|
||||
"noLogsToExport": "没有日志可导出",
|
||||
"exportAsCsv": "导出为 CSV",
|
||||
"exportAsText": "导出为文本",
|
||||
"receivedRfc3339": "收到 (RFC3339)",
|
||||
"buildTime": "构建时间",
|
||||
"downloadUrlNotAvailable": "下载链接不可用",
|
||||
"cannotOpenDownloadUrl": "无法打开下载链接",
|
||||
"updateCheckIsOnlyAvailableOnAndroid": "更新检查仅在 Android 上可用",
|
||||
"youAreRunningTheLatestVersion": "您正在使用最新版本",
|
||||
"updateAvailableButDownloadUrlNotFound": "有更新可用但未找到下载链接",
|
||||
"startTictactoe": "开始井字棋",
|
||||
"tictactoeUnavailable": "井字棋不可用",
|
||||
"tictactoeOpponentUnknown": "井字棋:对手未知",
|
||||
"tictactoeWaitingForStart": "井字棋:等待开始",
|
||||
"acceptsShareLinks": "接受分享链接",
|
||||
"supportsRawHex": "支持原始十六进制",
|
||||
"clipboardfriendly": "剪贴板友好",
|
||||
"captured": "已捕获",
|
||||
"size": "大小",
|
||||
"noCustomChannelsToClear": "没有自定义频道可清除。",
|
||||
"noDeviceContactsToClear": "没有设备联系人可清除。"
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart'
|
||||
show TargetPlatform, defaultTargetPlatform, kIsWeb;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_localizations/flutter_localizations.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||
import 'providers/connection_provider.dart';
|
||||
import 'providers/contacts_provider.dart';
|
||||
import 'providers/messages_provider.dart';
|
||||
@@ -19,14 +23,21 @@ import 'services/voice_codec_service.dart';
|
||||
import 'services/voice_player_service.dart';
|
||||
import 'services/notification_service.dart';
|
||||
import 'services/locale_preferences.dart';
|
||||
import 'services/mesh_map_nodes_service.dart';
|
||||
import 'services/profile_manager.dart';
|
||||
import 'services/profile_workspace_coordinator.dart';
|
||||
import 'services/profiles_feature_service.dart';
|
||||
import 'services/update_checker_service.dart';
|
||||
import 'services/wizard_preferences.dart';
|
||||
import 'screens/discovery_screen.dart';
|
||||
import 'screens/home_screen.dart';
|
||||
import 'screens/welcome_wizard_screen.dart';
|
||||
import 'theme/app_theme.dart';
|
||||
import 'l10n/app_localizations.dart';
|
||||
|
||||
void main() {
|
||||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
await WakelockPlus.enable();
|
||||
runApp(const MeshCoreSarApp());
|
||||
}
|
||||
|
||||
@@ -38,11 +49,13 @@ class MeshCoreSarApp extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||
AppThemeMode _themeMode = AppThemeMode.system;
|
||||
Locale? _locale;
|
||||
bool _isInitialized = false;
|
||||
bool _shouldShowPermissionDialog = false;
|
||||
bool _wizardCompleted = true; // Will be updated in _initializeApp()
|
||||
String? _pendingNotificationPayload;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -51,6 +64,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
}
|
||||
|
||||
Future<void> _initializeApp() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
final activeProfileId =
|
||||
prefs.getString(ProfileManager.activeProfileIdKey) ?? 'default';
|
||||
await ProfileStorageScope.bootstrap(
|
||||
profilesEnabled: profilesEnabled,
|
||||
activeProfileId: activeProfileId,
|
||||
);
|
||||
await _loadThemePreference();
|
||||
await _loadLocalePreference();
|
||||
|
||||
@@ -62,6 +83,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
|
||||
// Set up notification tap handler for update notifications
|
||||
NotificationService().onNotificationTapped = _handleNotificationTap;
|
||||
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
||||
|
||||
// Check if we need to request location permissions
|
||||
await _checkLocationPermissions();
|
||||
@@ -70,10 +92,21 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
// Shows notification if update is available
|
||||
_checkForUpdates();
|
||||
|
||||
// Refresh remote mesh node cache in background when the app starts.
|
||||
unawaited(MeshMapNodesService.syncInBackgroundIfStale());
|
||||
|
||||
setState(() {
|
||||
_wizardCompleted = wizardCompleted;
|
||||
_isInitialized = true;
|
||||
});
|
||||
|
||||
if (_pendingNotificationPayload != null) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
final payload = _pendingNotificationPayload;
|
||||
_pendingNotificationPayload = null;
|
||||
_handleNotificationTap(payload);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle notification tap
|
||||
@@ -82,10 +115,27 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
|
||||
debugPrint('[Main] Notification tapped: $payload');
|
||||
|
||||
if (!_wizardCompleted) {
|
||||
_pendingNotificationPayload = payload;
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle update notification tap
|
||||
if (payload.startsWith('update:')) {
|
||||
final downloadUrl = payload.substring(7); // Remove 'update:' prefix
|
||||
_launchUpdateDownload(downloadUrl);
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.startsWith('discovery:')) {
|
||||
final navigator = _navigatorKey.currentState;
|
||||
if (navigator == null) {
|
||||
_pendingNotificationPayload = payload;
|
||||
return;
|
||||
}
|
||||
navigator.push(
|
||||
MaterialPageRoute(builder: (context) => const DiscoveryScreen()),
|
||||
);
|
||||
}
|
||||
// SAR and message notifications handled by their respective providers
|
||||
}
|
||||
@@ -158,7 +208,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
/// Shows notification if update is available
|
||||
Future<void> _checkForUpdates() async {
|
||||
// Only check for updates on Android
|
||||
if (!Platform.isAndroid) {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||
debugPrint('[UpdateChecker] Skipping update check (not Android)');
|
||||
return;
|
||||
}
|
||||
@@ -235,6 +285,13 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
),
|
||||
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||
ChangeNotifierProvider(
|
||||
create: (_) {
|
||||
final manager = ProfileManager();
|
||||
manager.initialize();
|
||||
return manager;
|
||||
},
|
||||
),
|
||||
|
||||
// Voice provider (packet reassembly + playback)
|
||||
ChangeNotifierProvider(
|
||||
@@ -287,6 +344,37 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
imageProvider: context.read<ip.ImageProvider>(),
|
||||
),
|
||||
),
|
||||
ProxyProvider6<
|
||||
ProfileManager,
|
||||
AppProvider,
|
||||
ConnectionProvider,
|
||||
ContactsProvider,
|
||||
MessagesProvider,
|
||||
MapProvider,
|
||||
ProfileWorkspaceCoordinator
|
||||
>(
|
||||
update:
|
||||
(
|
||||
context,
|
||||
profileManager,
|
||||
appProvider,
|
||||
connectionProvider,
|
||||
contactsProvider,
|
||||
messagesProvider,
|
||||
mapProvider,
|
||||
previous,
|
||||
) => ProfileWorkspaceCoordinator(
|
||||
profileManager: profileManager,
|
||||
connectionProvider: connectionProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
messagesProvider: messagesProvider,
|
||||
sensorsProvider: context.read<SensorsProvider>(),
|
||||
mapProvider: mapProvider,
|
||||
drawingProvider: context.read<DrawingProvider>(),
|
||||
channelsProvider: context.read<ChannelsProvider>(),
|
||||
appProvider: appProvider,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: _buildMaterialApp(),
|
||||
);
|
||||
@@ -297,6 +385,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
builder: (context) {
|
||||
final systemBrightness = MediaQuery.platformBrightnessOf(context);
|
||||
final materialApp = MaterialApp(
|
||||
navigatorKey: _navigatorKey,
|
||||
key: ValueKey<String?>(
|
||||
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
|
||||
),
|
||||
@@ -324,7 +413,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||
|
||||
// Wrap in SafeArea for Android only to fix navigation bar overlap (API ≥36)
|
||||
// iOS doesn't need SafeArea wrapping (causes extra black space at bottom)
|
||||
if (Platform.isAndroid) {
|
||||
if (!kIsWeb && defaultTargetPlatform == TargetPlatform.android) {
|
||||
return SafeArea(
|
||||
left: false,
|
||||
right: false,
|
||||
|
||||
@@ -57,9 +57,7 @@ class Channel {
|
||||
} else {
|
||||
// Normal channel: require explicit secret
|
||||
if (explicitSecret == null || explicitSecret.length != 16) {
|
||||
throw ArgumentError(
|
||||
'Normal channels require a 16-byte secret',
|
||||
);
|
||||
throw ArgumentError('Normal channels require a 16-byte secret');
|
||||
}
|
||||
return Channel(
|
||||
index: index,
|
||||
@@ -78,6 +76,18 @@ class Channel {
|
||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
||||
}
|
||||
|
||||
static bool isHashChannelName(String channelName) {
|
||||
return channelName.trim().startsWith('#');
|
||||
}
|
||||
|
||||
static String pskBase64ForHashChannelName(String channelName) {
|
||||
final normalized = channelName.trim();
|
||||
if (!isHashChannelName(normalized)) {
|
||||
throw ArgumentError('Only #channels can export derived psk_base64');
|
||||
}
|
||||
return base64.encode(_generateHashChannelSecret(normalized));
|
||||
}
|
||||
|
||||
/// Create the default public channel (channel 0)
|
||||
/// Uses the well-known pre-shared key from MeshCore
|
||||
factory Channel.publicChannel() {
|
||||
@@ -85,8 +95,22 @@ class Channel {
|
||||
index: 0,
|
||||
name: 'Public Channel',
|
||||
secret: Uint8List.fromList([
|
||||
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
||||
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
|
||||
0x8b,
|
||||
0x33,
|
||||
0x87,
|
||||
0xe9,
|
||||
0xc5,
|
||||
0xcd,
|
||||
0xea,
|
||||
0x6a,
|
||||
0xc9,
|
||||
0xe5,
|
||||
0xed,
|
||||
0xba,
|
||||
0xa1,
|
||||
0x15,
|
||||
0xcd,
|
||||
0x72,
|
||||
]),
|
||||
flags: null,
|
||||
);
|
||||
@@ -95,6 +119,9 @@ class Channel {
|
||||
/// Check if this is a hash-based channel (name starts with '#')
|
||||
bool get isHashChannel => name.startsWith('#');
|
||||
|
||||
/// Base64-encoded PSK for sharing with firmware CLI and related tooling.
|
||||
String get pskBase64 => base64.encode(secret);
|
||||
|
||||
/// Display name for the channel
|
||||
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
||||
String get displayName {
|
||||
@@ -131,12 +158,7 @@ class Channel {
|
||||
}
|
||||
|
||||
/// Create a copy with modified fields
|
||||
Channel copyWith({
|
||||
int? index,
|
||||
String? name,
|
||||
Uint8List? secret,
|
||||
int? flags,
|
||||
}) {
|
||||
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
|
||||
return Channel(
|
||||
index: index ?? this.index,
|
||||
name: name ?? this.name,
|
||||
|
||||
528
lib/models/config_profile.dart
Normal file
528
lib/models/config_profile.dart
Normal file
@@ -0,0 +1,528 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'channel.dart';
|
||||
|
||||
@immutable
|
||||
class DeviceConfigProfileSection {
|
||||
final int? frequencyKhz;
|
||||
final int? bandwidth;
|
||||
final int? spreadingFactor;
|
||||
final int? codingRate;
|
||||
final bool? repeatEnabled;
|
||||
final int? txPower;
|
||||
final int? telemetryModes;
|
||||
final int? advertLocationPolicy;
|
||||
final int? multiAcks;
|
||||
final bool? manualAddContacts;
|
||||
final bool? autoAddUsers;
|
||||
final bool? autoAddRepeaters;
|
||||
final bool? autoAddRoomServers;
|
||||
final bool? autoAddSensors;
|
||||
final bool? autoAddOverwriteOldest;
|
||||
final double? publicLatitude;
|
||||
final double? publicLongitude;
|
||||
|
||||
const DeviceConfigProfileSection({
|
||||
this.frequencyKhz,
|
||||
this.bandwidth,
|
||||
this.spreadingFactor,
|
||||
this.codingRate,
|
||||
this.repeatEnabled,
|
||||
this.txPower,
|
||||
this.telemetryModes,
|
||||
this.advertLocationPolicy,
|
||||
this.multiAcks,
|
||||
this.manualAddContacts,
|
||||
this.autoAddUsers,
|
||||
this.autoAddRepeaters,
|
||||
this.autoAddRoomServers,
|
||||
this.autoAddSensors,
|
||||
this.autoAddOverwriteOldest,
|
||||
this.publicLatitude,
|
||||
this.publicLongitude,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
frequencyKhz == null &&
|
||||
bandwidth == null &&
|
||||
spreadingFactor == null &&
|
||||
codingRate == null &&
|
||||
repeatEnabled == null &&
|
||||
txPower == null &&
|
||||
telemetryModes == null &&
|
||||
advertLocationPolicy == null &&
|
||||
multiAcks == null &&
|
||||
manualAddContacts == null &&
|
||||
autoAddUsers == null &&
|
||||
autoAddRepeaters == null &&
|
||||
autoAddRoomServers == null &&
|
||||
autoAddSensors == null &&
|
||||
autoAddOverwriteOldest == null &&
|
||||
publicLatitude == null &&
|
||||
publicLongitude == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'frequencyKhz': frequencyKhz,
|
||||
'bandwidth': bandwidth,
|
||||
'spreadingFactor': spreadingFactor,
|
||||
'codingRate': codingRate,
|
||||
'repeatEnabled': repeatEnabled,
|
||||
'txPower': txPower,
|
||||
'telemetryModes': telemetryModes,
|
||||
'advertLocationPolicy': advertLocationPolicy,
|
||||
'multiAcks': multiAcks,
|
||||
'manualAddContacts': manualAddContacts,
|
||||
'autoAddUsers': autoAddUsers,
|
||||
'autoAddRepeaters': autoAddRepeaters,
|
||||
'autoAddRoomServers': autoAddRoomServers,
|
||||
'autoAddSensors': autoAddSensors,
|
||||
'autoAddOverwriteOldest': autoAddOverwriteOldest,
|
||||
'publicLatitude': publicLatitude,
|
||||
'publicLongitude': publicLongitude,
|
||||
};
|
||||
|
||||
factory DeviceConfigProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceConfigProfileSection(
|
||||
frequencyKhz: json['frequencyKhz'] as int?,
|
||||
bandwidth: json['bandwidth'] as int?,
|
||||
spreadingFactor: json['spreadingFactor'] as int?,
|
||||
codingRate: json['codingRate'] as int?,
|
||||
repeatEnabled: json['repeatEnabled'] as bool?,
|
||||
txPower: json['txPower'] as int?,
|
||||
telemetryModes: json['telemetryModes'] as int?,
|
||||
advertLocationPolicy: json['advertLocationPolicy'] as int?,
|
||||
multiAcks: json['multiAcks'] as int?,
|
||||
manualAddContacts: json['manualAddContacts'] as bool?,
|
||||
autoAddUsers: json['autoAddUsers'] as bool?,
|
||||
autoAddRepeaters: json['autoAddRepeaters'] as bool?,
|
||||
autoAddRoomServers: json['autoAddRoomServers'] as bool?,
|
||||
autoAddSensors: json['autoAddSensors'] as bool?,
|
||||
autoAddOverwriteOldest: json['autoAddOverwriteOldest'] as bool?,
|
||||
publicLatitude: (json['publicLatitude'] as num?)?.toDouble(),
|
||||
publicLongitude: (json['publicLongitude'] as num?)?.toDouble(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class AppSettingsProfileSection {
|
||||
final bool? mapEnabled;
|
||||
final bool? contactsEnabled;
|
||||
final bool? sensorsEnabled;
|
||||
final bool? voiceSilenceTrimmingEnabled;
|
||||
final bool? voiceBandPassFilterEnabled;
|
||||
final bool? voiceCompressorEnabled;
|
||||
final bool? voiceLimiterEnabled;
|
||||
final bool? voiceAutoGainEnabled;
|
||||
final bool? voiceEchoCancellationEnabled;
|
||||
final bool? voiceNoiseSuppressionEnabled;
|
||||
final double? messageFontScale;
|
||||
final bool? autoRouteRotationEnabled;
|
||||
final bool? clearPathOnMaxRetry;
|
||||
final bool? nearestRelayFallbackEnabled;
|
||||
final int? voiceBitrate;
|
||||
final int? routeHashSize;
|
||||
final int? imageMaxSize;
|
||||
final int? imageCompression;
|
||||
final bool? imageGrayscale;
|
||||
final bool? imageUltraMode;
|
||||
final bool? showRxTxIndicators;
|
||||
final bool? fastLocationUpdatesEnabled;
|
||||
final double? fastLocationMovementThresholdMeters;
|
||||
final int? fastLocationActiveCadenceSeconds;
|
||||
|
||||
const AppSettingsProfileSection({
|
||||
this.mapEnabled,
|
||||
this.contactsEnabled,
|
||||
this.sensorsEnabled,
|
||||
this.voiceSilenceTrimmingEnabled,
|
||||
this.voiceBandPassFilterEnabled,
|
||||
this.voiceCompressorEnabled,
|
||||
this.voiceLimiterEnabled,
|
||||
this.voiceAutoGainEnabled,
|
||||
this.voiceEchoCancellationEnabled,
|
||||
this.voiceNoiseSuppressionEnabled,
|
||||
this.messageFontScale,
|
||||
this.autoRouteRotationEnabled,
|
||||
this.clearPathOnMaxRetry,
|
||||
this.nearestRelayFallbackEnabled,
|
||||
this.voiceBitrate,
|
||||
this.routeHashSize,
|
||||
this.imageMaxSize,
|
||||
this.imageCompression,
|
||||
this.imageGrayscale,
|
||||
this.imageUltraMode,
|
||||
this.showRxTxIndicators,
|
||||
this.fastLocationUpdatesEnabled,
|
||||
this.fastLocationMovementThresholdMeters,
|
||||
this.fastLocationActiveCadenceSeconds,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
mapEnabled == null &&
|
||||
contactsEnabled == null &&
|
||||
sensorsEnabled == null &&
|
||||
voiceSilenceTrimmingEnabled == null &&
|
||||
voiceBandPassFilterEnabled == null &&
|
||||
voiceCompressorEnabled == null &&
|
||||
voiceLimiterEnabled == null &&
|
||||
voiceAutoGainEnabled == null &&
|
||||
voiceEchoCancellationEnabled == null &&
|
||||
voiceNoiseSuppressionEnabled == null &&
|
||||
messageFontScale == null &&
|
||||
autoRouteRotationEnabled == null &&
|
||||
clearPathOnMaxRetry == null &&
|
||||
nearestRelayFallbackEnabled == null &&
|
||||
voiceBitrate == null &&
|
||||
routeHashSize == null &&
|
||||
imageMaxSize == null &&
|
||||
imageCompression == null &&
|
||||
imageGrayscale == null &&
|
||||
imageUltraMode == null &&
|
||||
showRxTxIndicators == null &&
|
||||
fastLocationUpdatesEnabled == null &&
|
||||
fastLocationMovementThresholdMeters == null &&
|
||||
fastLocationActiveCadenceSeconds == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'mapEnabled': mapEnabled,
|
||||
'contactsEnabled': contactsEnabled,
|
||||
'sensorsEnabled': sensorsEnabled,
|
||||
'voiceSilenceTrimmingEnabled': voiceSilenceTrimmingEnabled,
|
||||
'voiceBandPassFilterEnabled': voiceBandPassFilterEnabled,
|
||||
'voiceCompressorEnabled': voiceCompressorEnabled,
|
||||
'voiceLimiterEnabled': voiceLimiterEnabled,
|
||||
'voiceAutoGainEnabled': voiceAutoGainEnabled,
|
||||
'voiceEchoCancellationEnabled': voiceEchoCancellationEnabled,
|
||||
'voiceNoiseSuppressionEnabled': voiceNoiseSuppressionEnabled,
|
||||
'messageFontScale': messageFontScale,
|
||||
'autoRouteRotationEnabled': autoRouteRotationEnabled,
|
||||
'clearPathOnMaxRetry': clearPathOnMaxRetry,
|
||||
'nearestRelayFallbackEnabled': nearestRelayFallbackEnabled,
|
||||
'voiceBitrate': voiceBitrate,
|
||||
'routeHashSize': routeHashSize,
|
||||
'imageMaxSize': imageMaxSize,
|
||||
'imageCompression': imageCompression,
|
||||
'imageGrayscale': imageGrayscale,
|
||||
'imageUltraMode': imageUltraMode,
|
||||
'showRxTxIndicators': showRxTxIndicators,
|
||||
'fastLocationUpdatesEnabled': fastLocationUpdatesEnabled,
|
||||
'fastLocationMovementThresholdMeters': fastLocationMovementThresholdMeters,
|
||||
'fastLocationActiveCadenceSeconds': fastLocationActiveCadenceSeconds,
|
||||
};
|
||||
|
||||
factory AppSettingsProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
return AppSettingsProfileSection(
|
||||
mapEnabled: json['mapEnabled'] as bool?,
|
||||
contactsEnabled: json['contactsEnabled'] as bool?,
|
||||
sensorsEnabled: json['sensorsEnabled'] as bool?,
|
||||
voiceSilenceTrimmingEnabled: json['voiceSilenceTrimmingEnabled'] as bool?,
|
||||
voiceBandPassFilterEnabled: json['voiceBandPassFilterEnabled'] as bool?,
|
||||
voiceCompressorEnabled: json['voiceCompressorEnabled'] as bool?,
|
||||
voiceLimiterEnabled: json['voiceLimiterEnabled'] as bool?,
|
||||
voiceAutoGainEnabled: json['voiceAutoGainEnabled'] as bool?,
|
||||
voiceEchoCancellationEnabled:
|
||||
json['voiceEchoCancellationEnabled'] as bool?,
|
||||
voiceNoiseSuppressionEnabled:
|
||||
json['voiceNoiseSuppressionEnabled'] as bool?,
|
||||
messageFontScale: (json['messageFontScale'] as num?)?.toDouble(),
|
||||
autoRouteRotationEnabled: json['autoRouteRotationEnabled'] as bool?,
|
||||
clearPathOnMaxRetry: json['clearPathOnMaxRetry'] as bool?,
|
||||
nearestRelayFallbackEnabled: json['nearestRelayFallbackEnabled'] as bool?,
|
||||
voiceBitrate: json['voiceBitrate'] as int?,
|
||||
routeHashSize: json['routeHashSize'] as int?,
|
||||
imageMaxSize: json['imageMaxSize'] as int?,
|
||||
imageCompression: json['imageCompression'] as int?,
|
||||
imageGrayscale: json['imageGrayscale'] as bool?,
|
||||
imageUltraMode: json['imageUltraMode'] as bool?,
|
||||
showRxTxIndicators: json['showRxTxIndicators'] as bool?,
|
||||
fastLocationUpdatesEnabled: json['fastLocationUpdatesEnabled'] as bool?,
|
||||
fastLocationMovementThresholdMeters:
|
||||
(json['fastLocationMovementThresholdMeters'] as num?)?.toDouble(),
|
||||
fastLocationActiveCadenceSeconds:
|
||||
json['fastLocationActiveCadenceSeconds'] as int?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class MapWorkspaceProfileSection {
|
||||
final Map<String, dynamic>? mapPrefs;
|
||||
final List<Map<String, dynamic>> drawings;
|
||||
final Map<String, dynamic>? currentTrail;
|
||||
final List<Map<String, dynamic>> trailHistory;
|
||||
final Map<String, dynamic>? importedTrail;
|
||||
final bool? isTrailVisible;
|
||||
final bool? showCadastralOverlay;
|
||||
final bool? showForestRoadsOverlay;
|
||||
final bool? showHikingTrailsOverlay;
|
||||
final bool? showMainRoadsOverlay;
|
||||
final bool? showHouseNumbersOverlay;
|
||||
final bool? showFireHazardZonesOverlay;
|
||||
final bool? showHistoricalFiresOverlay;
|
||||
final bool? showFirebreaksOverlay;
|
||||
final bool? showKrasFireZonesOverlay;
|
||||
final bool? showPlaceNamesOverlay;
|
||||
final bool? showMunicipalityBordersOverlay;
|
||||
final bool? showAllContactTrails;
|
||||
final bool? hideRepeatersOnMap;
|
||||
|
||||
const MapWorkspaceProfileSection({
|
||||
this.mapPrefs,
|
||||
this.drawings = const [],
|
||||
this.currentTrail,
|
||||
this.trailHistory = const [],
|
||||
this.importedTrail,
|
||||
this.isTrailVisible,
|
||||
this.showCadastralOverlay,
|
||||
this.showForestRoadsOverlay,
|
||||
this.showHikingTrailsOverlay,
|
||||
this.showMainRoadsOverlay,
|
||||
this.showHouseNumbersOverlay,
|
||||
this.showFireHazardZonesOverlay,
|
||||
this.showHistoricalFiresOverlay,
|
||||
this.showFirebreaksOverlay,
|
||||
this.showKrasFireZonesOverlay,
|
||||
this.showPlaceNamesOverlay,
|
||||
this.showMunicipalityBordersOverlay,
|
||||
this.showAllContactTrails,
|
||||
this.hideRepeatersOnMap,
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
mapPrefs == null &&
|
||||
drawings.isEmpty &&
|
||||
currentTrail == null &&
|
||||
trailHistory.isEmpty &&
|
||||
importedTrail == null &&
|
||||
isTrailVisible == null &&
|
||||
showCadastralOverlay == null &&
|
||||
showForestRoadsOverlay == null &&
|
||||
showHikingTrailsOverlay == null &&
|
||||
showMainRoadsOverlay == null &&
|
||||
showHouseNumbersOverlay == null &&
|
||||
showFireHazardZonesOverlay == null &&
|
||||
showHistoricalFiresOverlay == null &&
|
||||
showFirebreaksOverlay == null &&
|
||||
showKrasFireZonesOverlay == null &&
|
||||
showPlaceNamesOverlay == null &&
|
||||
showMunicipalityBordersOverlay == null &&
|
||||
showAllContactTrails == null &&
|
||||
hideRepeatersOnMap == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'mapPrefs': mapPrefs,
|
||||
'drawings': drawings,
|
||||
'currentTrail': currentTrail,
|
||||
'trailHistory': trailHistory,
|
||||
'importedTrail': importedTrail,
|
||||
'isTrailVisible': isTrailVisible,
|
||||
'showCadastralOverlay': showCadastralOverlay,
|
||||
'showForestRoadsOverlay': showForestRoadsOverlay,
|
||||
'showHikingTrailsOverlay': showHikingTrailsOverlay,
|
||||
'showMainRoadsOverlay': showMainRoadsOverlay,
|
||||
'showHouseNumbersOverlay': showHouseNumbersOverlay,
|
||||
'showFireHazardZonesOverlay': showFireHazardZonesOverlay,
|
||||
'showHistoricalFiresOverlay': showHistoricalFiresOverlay,
|
||||
'showFirebreaksOverlay': showFirebreaksOverlay,
|
||||
'showKrasFireZonesOverlay': showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': showMunicipalityBordersOverlay,
|
||||
'showAllContactTrails': showAllContactTrails,
|
||||
'hideRepeatersOnMap': hideRepeatersOnMap,
|
||||
};
|
||||
|
||||
factory MapWorkspaceProfileSection.fromJson(Map<String, dynamic> json) {
|
||||
List<Map<String, dynamic>> decodeList(String key) {
|
||||
final raw = json[key] as List<dynamic>? ?? const [];
|
||||
return raw.whereType<Map<String, dynamic>>().toList();
|
||||
}
|
||||
|
||||
return MapWorkspaceProfileSection(
|
||||
mapPrefs: json['mapPrefs'] as Map<String, dynamic>?,
|
||||
drawings: decodeList('drawings'),
|
||||
currentTrail: json['currentTrail'] as Map<String, dynamic>?,
|
||||
trailHistory: decodeList('trailHistory'),
|
||||
importedTrail: json['importedTrail'] as Map<String, dynamic>?,
|
||||
isTrailVisible: json['isTrailVisible'] as bool?,
|
||||
showCadastralOverlay: json['showCadastralOverlay'] as bool?,
|
||||
showForestRoadsOverlay: json['showForestRoadsOverlay'] as bool?,
|
||||
showHikingTrailsOverlay: json['showHikingTrailsOverlay'] as bool?,
|
||||
showMainRoadsOverlay: json['showMainRoadsOverlay'] as bool?,
|
||||
showHouseNumbersOverlay: json['showHouseNumbersOverlay'] as bool?,
|
||||
showFireHazardZonesOverlay: json['showFireHazardZonesOverlay'] as bool?,
|
||||
showHistoricalFiresOverlay: json['showHistoricalFiresOverlay'] as bool?,
|
||||
showFirebreaksOverlay: json['showFirebreaksOverlay'] as bool?,
|
||||
showKrasFireZonesOverlay: json['showKrasFireZonesOverlay'] as bool?,
|
||||
showPlaceNamesOverlay: json['showPlaceNamesOverlay'] as bool?,
|
||||
showMunicipalityBordersOverlay:
|
||||
json['showMunicipalityBordersOverlay'] as bool?,
|
||||
showAllContactTrails: json['showAllContactTrails'] as bool?,
|
||||
hideRepeatersOnMap: json['hideRepeatersOnMap'] as bool?,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ConfigProfileSections {
|
||||
final DeviceConfigProfileSection? deviceConfig;
|
||||
final AppSettingsProfileSection? appSettings;
|
||||
final MapWorkspaceProfileSection? mapWorkspace;
|
||||
final List<Channel> channels;
|
||||
|
||||
const ConfigProfileSections({
|
||||
this.deviceConfig,
|
||||
this.appSettings,
|
||||
this.mapWorkspace,
|
||||
this.channels = const [],
|
||||
});
|
||||
|
||||
bool get isEmpty =>
|
||||
(deviceConfig == null || deviceConfig!.isEmpty) &&
|
||||
(appSettings == null || appSettings!.isEmpty) &&
|
||||
(mapWorkspace == null || mapWorkspace!.isEmpty) &&
|
||||
channels.isEmpty;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'deviceConfig': deviceConfig?.toJson(),
|
||||
'appSettings': appSettings?.toJson(),
|
||||
'mapWorkspace': mapWorkspace?.toJson(),
|
||||
'channels': channels.map((channel) => channel.toJson()).toList(),
|
||||
};
|
||||
|
||||
factory ConfigProfileSections.fromJson(Map<String, dynamic> json) {
|
||||
final rawChannels = json['channels'] as List<dynamic>? ?? const [];
|
||||
return ConfigProfileSections(
|
||||
deviceConfig: json['deviceConfig'] is Map<String, dynamic>
|
||||
? DeviceConfigProfileSection.fromJson(
|
||||
json['deviceConfig'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
appSettings: json['appSettings'] is Map<String, dynamic>
|
||||
? AppSettingsProfileSection.fromJson(
|
||||
json['appSettings'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
mapWorkspace: json['mapWorkspace'] is Map<String, dynamic>
|
||||
? MapWorkspaceProfileSection.fromJson(
|
||||
json['mapWorkspace'] as Map<String, dynamic>,
|
||||
)
|
||||
: null,
|
||||
channels: rawChannels
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(Channel.fromJson)
|
||||
.toList(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ConfigProfile {
|
||||
static const String defaultProfileId = 'default';
|
||||
|
||||
final String id;
|
||||
final String name;
|
||||
final DateTime createdAt;
|
||||
final DateTime updatedAt;
|
||||
final String? notes;
|
||||
final ConfigProfileSections sections;
|
||||
|
||||
const ConfigProfile({
|
||||
required this.id,
|
||||
required this.name,
|
||||
required this.createdAt,
|
||||
required this.updatedAt,
|
||||
required this.sections,
|
||||
this.notes,
|
||||
});
|
||||
|
||||
bool get isDefault => id == defaultProfileId;
|
||||
|
||||
factory ConfigProfile.defaultProfile() {
|
||||
final now = DateTime.now();
|
||||
return ConfigProfile(
|
||||
id: defaultProfileId,
|
||||
name: 'Default',
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
sections: const ConfigProfileSections(),
|
||||
);
|
||||
}
|
||||
|
||||
ConfigProfile copyWith({
|
||||
String? id,
|
||||
String? name,
|
||||
DateTime? createdAt,
|
||||
DateTime? updatedAt,
|
||||
String? notes,
|
||||
ConfigProfileSections? sections,
|
||||
}) {
|
||||
return ConfigProfile(
|
||||
id: id ?? this.id,
|
||||
name: name ?? this.name,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
updatedAt: updatedAt ?? this.updatedAt,
|
||||
notes: notes ?? this.notes,
|
||||
sections: sections ?? this.sections,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
'notes': notes,
|
||||
'sections': sections.toJson(),
|
||||
};
|
||||
|
||||
factory ConfigProfile.fromJson(Map<String, dynamic> json) {
|
||||
return ConfigProfile(
|
||||
id: json['id'] as String,
|
||||
name: json['name'] as String,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
notes: json['notes'] as String?,
|
||||
sections: ConfigProfileSections.fromJson(
|
||||
json['sections'] as Map<String, dynamic>? ?? const {},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProfileDiff {
|
||||
final List<String> changedSections;
|
||||
|
||||
const ProfileDiff({required this.changedSections});
|
||||
}
|
||||
|
||||
@immutable
|
||||
class ProfileTransferRecord {
|
||||
final String profileId;
|
||||
final String direction;
|
||||
final DateTime timestamp;
|
||||
final String detail;
|
||||
|
||||
const ProfileTransferRecord({
|
||||
required this.profileId,
|
||||
required this.direction,
|
||||
required this.timestamp,
|
||||
required this.detail,
|
||||
});
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'profileId': profileId,
|
||||
'direction': direction,
|
||||
'timestamp': timestamp.toIso8601String(),
|
||||
'detail': detail,
|
||||
};
|
||||
|
||||
factory ProfileTransferRecord.fromJson(Map<String, dynamic> json) {
|
||||
return ProfileTransferRecord(
|
||||
profileId: json['profileId'] as String,
|
||||
direction: json['direction'] as String,
|
||||
timestamp: DateTime.parse(json['timestamp'] as String),
|
||||
detail: json['detail'] as String,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -141,6 +141,23 @@ class ContactRouteCodec {
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute direct({int hashSize = 1}) {
|
||||
if (hashSize < 1 || hashSize > maxHashSize) {
|
||||
throw ArgumentError.value(hashSize, 'hashSize', 'Must be 1, 2, or 3.');
|
||||
}
|
||||
|
||||
final encodedPathLen = (hashSize - 1) << 6;
|
||||
return ParsedContactRoute(
|
||||
canonicalText: '',
|
||||
hashSize: hashSize,
|
||||
hopCount: 0,
|
||||
encodedPathLen: encodedPathLen,
|
||||
signedEncodedPathLen: toSignedDescriptor(encodedPathLen),
|
||||
pathBytes: Uint8List(0),
|
||||
paddedPathBytes: Uint8List(maxPathBytes),
|
||||
);
|
||||
}
|
||||
|
||||
static ParsedContactRoute? fromContact(Contact contact) {
|
||||
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
||||
return null;
|
||||
@@ -240,6 +257,5 @@ extension ContactLocalization on Contact {
|
||||
return '$routeHopCount hop${routeHopCount == 1 ? '' : 's'} via $routeHashSize-byte hashes';
|
||||
}
|
||||
|
||||
bool get routeSupportsLegacyRawTransport =>
|
||||
routeHasPath && routeSignedPathLen >= 0;
|
||||
bool get routeSupportsLegacyRawTransport => routeHasPath;
|
||||
}
|
||||
|
||||
39
lib/models/contact_group.dart
Normal file
39
lib/models/contact_group.dart
Normal file
@@ -0,0 +1,39 @@
|
||||
class SavedContactGroup {
|
||||
final String id;
|
||||
final String sectionKey;
|
||||
final String label;
|
||||
final String query;
|
||||
final DateTime createdAt;
|
||||
final List<String>? matchPrefixes;
|
||||
final bool isAutoGroup;
|
||||
|
||||
const SavedContactGroup({
|
||||
required this.id,
|
||||
required this.sectionKey,
|
||||
required this.label,
|
||||
required this.query,
|
||||
required this.createdAt,
|
||||
this.matchPrefixes,
|
||||
this.isAutoGroup = false,
|
||||
});
|
||||
|
||||
SavedContactGroup copyWith({
|
||||
String? id,
|
||||
String? sectionKey,
|
||||
String? label,
|
||||
String? query,
|
||||
DateTime? createdAt,
|
||||
List<String>? matchPrefixes,
|
||||
bool? isAutoGroup,
|
||||
}) {
|
||||
return SavedContactGroup(
|
||||
id: id ?? this.id,
|
||||
sectionKey: sectionKey ?? this.sectionKey,
|
||||
label: label ?? this.label,
|
||||
query: query ?? this.query,
|
||||
createdAt: createdAt ?? this.createdAt,
|
||||
matchPrefixes: matchPrefixes ?? this.matchPrefixes,
|
||||
isAutoGroup: isAutoGroup ?? this.isAutoGroup,
|
||||
);
|
||||
}
|
||||
}
|
||||
151
lib/models/custom_map_config.dart
Normal file
151
lib/models/custom_map_config.dart
Normal file
@@ -0,0 +1,151 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
class CustomMapConfig {
|
||||
final String filePath;
|
||||
final String displayName;
|
||||
final String mapId;
|
||||
final int imageWidth;
|
||||
final int imageHeight;
|
||||
final double? metersPerPixel;
|
||||
final LatLng? calibrationPointA;
|
||||
final LatLng? calibrationPointB;
|
||||
|
||||
const CustomMapConfig({
|
||||
required this.filePath,
|
||||
required this.displayName,
|
||||
required this.mapId,
|
||||
required this.imageWidth,
|
||||
required this.imageHeight,
|
||||
this.metersPerPixel,
|
||||
this.calibrationPointA,
|
||||
this.calibrationPointB,
|
||||
});
|
||||
|
||||
bool get isCalibrated => metersPerPixel != null && metersPerPixel! > 0;
|
||||
|
||||
double get _displayScale {
|
||||
final heightScale = imageHeight / 90.0;
|
||||
final widthScale = imageWidth / 180.0;
|
||||
return math.max(1.0, math.max(heightScale, widthScale));
|
||||
}
|
||||
|
||||
double get _displayHeight => imageHeight / _displayScale;
|
||||
|
||||
double get _displayWidth => imageWidth / _displayScale;
|
||||
|
||||
LatLngBounds get bounds =>
|
||||
LatLngBounds(const LatLng(0, 0), LatLng(_displayHeight, _displayWidth));
|
||||
|
||||
LatLngBounds get displayBounds =>
|
||||
LatLngBounds(LatLng(_displayHeight, 0), LatLng(0, _displayWidth));
|
||||
|
||||
LatLng toDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
_displayHeight - (point.latitude / _displayScale),
|
||||
point.longitude / _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
LatLng fromDisplayPoint(LatLng point) {
|
||||
return LatLng(
|
||||
(_displayHeight - point.latitude) * _displayScale,
|
||||
point.longitude * _displayScale,
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'filePath': filePath,
|
||||
'displayName': displayName,
|
||||
'mapId': mapId,
|
||||
'imageWidth': imageWidth,
|
||||
'imageHeight': imageHeight,
|
||||
'metersPerPixel': metersPerPixel,
|
||||
'calibrationPointA': calibrationPointA != null
|
||||
? {
|
||||
'lat': calibrationPointA!.latitude,
|
||||
'lon': calibrationPointA!.longitude,
|
||||
}
|
||||
: null,
|
||||
'calibrationPointB': calibrationPointB != null
|
||||
? {
|
||||
'lat': calibrationPointB!.latitude,
|
||||
'lon': calibrationPointB!.longitude,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
CustomMapConfig copyWith({
|
||||
String? filePath,
|
||||
String? displayName,
|
||||
String? mapId,
|
||||
int? imageWidth,
|
||||
int? imageHeight,
|
||||
double? metersPerPixel,
|
||||
bool clearMetersPerPixel = false,
|
||||
LatLng? calibrationPointA,
|
||||
bool clearCalibrationPointA = false,
|
||||
LatLng? calibrationPointB,
|
||||
bool clearCalibrationPointB = false,
|
||||
}) {
|
||||
return CustomMapConfig(
|
||||
filePath: filePath ?? this.filePath,
|
||||
displayName: displayName ?? this.displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? this.mapId,
|
||||
imageWidth: imageWidth ?? this.imageWidth,
|
||||
imageHeight: imageHeight ?? this.imageHeight,
|
||||
metersPerPixel: clearMetersPerPixel
|
||||
? null
|
||||
: (metersPerPixel ?? this.metersPerPixel),
|
||||
calibrationPointA: clearCalibrationPointA
|
||||
? null
|
||||
: (calibrationPointA ?? this.calibrationPointA),
|
||||
calibrationPointB: clearCalibrationPointB
|
||||
? null
|
||||
: (calibrationPointB ?? this.calibrationPointB),
|
||||
);
|
||||
}
|
||||
|
||||
static CustomMapConfig? fromJson(Map<String, dynamic>? json) {
|
||||
if (json == null) return null;
|
||||
|
||||
final filePath = json['filePath'];
|
||||
final displayName = json['displayName'];
|
||||
final mapId = json['mapId'];
|
||||
final imageWidth = json['imageWidth'];
|
||||
final imageHeight = json['imageHeight'];
|
||||
if (filePath is! String ||
|
||||
displayName is! String ||
|
||||
mapId is! String ||
|
||||
imageWidth is! int ||
|
||||
imageHeight is! int) {
|
||||
return null;
|
||||
}
|
||||
|
||||
LatLng? parsePoint(dynamic raw) {
|
||||
if (raw is! Map<String, dynamic>) return null;
|
||||
final lat = raw['lat'];
|
||||
final lon = raw['lon'];
|
||||
if (lat is! num || lon is! num) return null;
|
||||
return LatLng(lat.toDouble(), lon.toDouble());
|
||||
}
|
||||
|
||||
final metersPerPixel = json['metersPerPixel'];
|
||||
return CustomMapConfig(
|
||||
filePath: filePath,
|
||||
displayName: displayName,
|
||||
mapId: normalizeCustomMapId(mapId) ?? mapId,
|
||||
imageWidth: imageWidth,
|
||||
imageHeight: imageHeight,
|
||||
metersPerPixel: metersPerPixel is num ? metersPerPixel.toDouble() : null,
|
||||
calibrationPointA: parsePoint(json['calibrationPointA']),
|
||||
calibrationPointB: parsePoint(json['calibrationPointB']),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,11 +14,11 @@ enum ConnectionMode {
|
||||
/// Direct BLE connection to MeshCore device (default)
|
||||
ble,
|
||||
|
||||
/// Act as SSE server - share BLE device with multiple clients
|
||||
sseServer,
|
||||
|
||||
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
|
||||
tcp,
|
||||
|
||||
/// USB serial connection (Android OTG or Web Serial API)
|
||||
usb,
|
||||
}
|
||||
|
||||
extension ConnectionModeExtension on ConnectionMode {
|
||||
@@ -26,10 +26,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
||||
switch (this) {
|
||||
case ConnectionMode.ble:
|
||||
return 'Direct (BLE)';
|
||||
case ConnectionMode.sseServer:
|
||||
return 'Share Device (Server)';
|
||||
case ConnectionMode.tcp:
|
||||
return 'Direct (WiFi)';
|
||||
case ConnectionMode.usb:
|
||||
return 'Direct (Serial)';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,10 +37,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
||||
switch (this) {
|
||||
case ConnectionMode.ble:
|
||||
return 'Direct BLE connection to MeshCore device';
|
||||
case ConnectionMode.sseServer:
|
||||
return 'Share BLE device with multiple clients over network';
|
||||
case ConnectionMode.tcp:
|
||||
return 'Direct WiFi/TCP connection to MeshCore device';
|
||||
case ConnectionMode.usb:
|
||||
return 'Direct serial connection to MeshCore device';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,11 @@ class DeviceInfo {
|
||||
final int? advLat;
|
||||
final int? advLon;
|
||||
final bool? manualAddContacts;
|
||||
final bool? autoAddUsers;
|
||||
final bool? autoAddRepeaters;
|
||||
final bool? autoAddRoomServers;
|
||||
final bool? autoAddSensors;
|
||||
final bool? autoAddOverwriteOldest;
|
||||
final int? radioFreq;
|
||||
final int? radioBw;
|
||||
final int? radioSf;
|
||||
@@ -113,6 +118,11 @@ class DeviceInfo {
|
||||
this.advLat,
|
||||
this.advLon,
|
||||
this.manualAddContacts,
|
||||
this.autoAddUsers,
|
||||
this.autoAddRepeaters,
|
||||
this.autoAddRoomServers,
|
||||
this.autoAddSensors,
|
||||
this.autoAddOverwriteOldest,
|
||||
this.radioFreq,
|
||||
this.radioBw,
|
||||
this.radioSf,
|
||||
@@ -239,6 +249,11 @@ class DeviceInfo {
|
||||
int? advLat,
|
||||
int? advLon,
|
||||
bool? manualAddContacts,
|
||||
bool? autoAddUsers,
|
||||
bool? autoAddRepeaters,
|
||||
bool? autoAddRoomServers,
|
||||
bool? autoAddSensors,
|
||||
bool? autoAddOverwriteOldest,
|
||||
int? radioFreq,
|
||||
int? radioBw,
|
||||
int? radioSf,
|
||||
@@ -278,6 +293,12 @@ class DeviceInfo {
|
||||
advLat: advLat ?? this.advLat,
|
||||
advLon: advLon ?? this.advLon,
|
||||
manualAddContacts: manualAddContacts ?? this.manualAddContacts,
|
||||
autoAddUsers: autoAddUsers ?? this.autoAddUsers,
|
||||
autoAddRepeaters: autoAddRepeaters ?? this.autoAddRepeaters,
|
||||
autoAddRoomServers: autoAddRoomServers ?? this.autoAddRoomServers,
|
||||
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
||||
autoAddOverwriteOldest:
|
||||
autoAddOverwriteOldest ?? this.autoAddOverwriteOldest,
|
||||
radioFreq: radioFreq ?? this.radioFreq,
|
||||
radioBw: radioBw ?? this.radioBw,
|
||||
radioSf: radioSf ?? this.radioSf,
|
||||
|
||||
11
lib/models/map_coordinate_space.dart
Normal file
11
lib/models/map_coordinate_space.dart
Normal file
@@ -0,0 +1,11 @@
|
||||
enum MapCoordinateSpace {
|
||||
geo,
|
||||
customMap;
|
||||
|
||||
static MapCoordinateSpace fromName(String? value) {
|
||||
return MapCoordinateSpace.values.firstWhere(
|
||||
(space) => space.name == value,
|
||||
orElse: () => MapCoordinateSpace.geo,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,35 +2,26 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
/// Drawing shape type
|
||||
enum DrawingShapeType {
|
||||
line,
|
||||
rectangle,
|
||||
}
|
||||
enum DrawingShapeType { line, rectangle }
|
||||
|
||||
/// Drawing color enum for compact network transmission
|
||||
enum DrawingColor {
|
||||
red, // 0
|
||||
blue, // 1
|
||||
green, // 2
|
||||
yellow, // 3
|
||||
orange, // 4
|
||||
purple, // 5
|
||||
pink, // 6
|
||||
cyan, // 7
|
||||
}
|
||||
enum DrawingColor { red, blue, green, yellow, orange, purple, pink, cyan }
|
||||
|
||||
/// Drawing colors available for user selection
|
||||
class DrawingColors {
|
||||
static const List<Color> palette = [
|
||||
Colors.red, // index 0
|
||||
Colors.blue, // index 1
|
||||
Colors.green, // index 2
|
||||
Colors.yellow, // index 3
|
||||
Colors.orange, // index 4
|
||||
Colors.purple, // index 5
|
||||
Colors.pink, // index 6
|
||||
Colors.cyan, // index 7
|
||||
Colors.red,
|
||||
Colors.blue,
|
||||
Colors.green,
|
||||
Colors.yellow,
|
||||
Colors.orange,
|
||||
Colors.purple,
|
||||
Colors.pink,
|
||||
Colors.cyan,
|
||||
];
|
||||
|
||||
static String colorToName(Color color) {
|
||||
@@ -45,37 +36,36 @@ class DrawingColors {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
/// Convert Color to enum index for network transmission
|
||||
static int colorToIndex(Color color) {
|
||||
for (int i = 0; i < palette.length; i++) {
|
||||
if (palette[i].toARGB32() == color.toARGB32()) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0; // Default to red if not found
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Convert enum index to Color for network reception
|
||||
static Color indexToColor(int index) {
|
||||
if (index >= 0 && index < palette.length) {
|
||||
return palette[index];
|
||||
}
|
||||
return palette[0]; // Default to red if invalid index
|
||||
return palette[0];
|
||||
}
|
||||
}
|
||||
|
||||
/// Base class for map drawings
|
||||
abstract class MapDrawing {
|
||||
final String id;
|
||||
final DrawingShapeType type;
|
||||
final Color color;
|
||||
final DateTime createdAt;
|
||||
final String? senderName; // Name of sender (null if local drawing)
|
||||
final bool isReceived; // True if drawing was received from another node
|
||||
final String? messageId; // ID of the source message (for navigation)
|
||||
final bool isShared; // Whether drawing has been broadcast over mesh
|
||||
final bool isSent; // Whether this is a sent drawing (vs received)
|
||||
final bool isHidden; // Temporary visibility toggle (session only, not persisted)
|
||||
final String? senderName;
|
||||
final bool isReceived;
|
||||
final String? messageId;
|
||||
final bool isShared;
|
||||
final bool isSent;
|
||||
final bool isHidden;
|
||||
final MapCoordinateSpace coordinateSpace;
|
||||
final String? mapId;
|
||||
|
||||
MapDrawing({
|
||||
required this.id,
|
||||
@@ -88,79 +78,79 @@ abstract class MapDrawing {
|
||||
this.isShared = false,
|
||||
this.isSent = false,
|
||||
this.isHidden = false,
|
||||
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||
this.mapId,
|
||||
});
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
bool get isCustomMap => coordinateSpace == MapCoordinateSpace.customMap;
|
||||
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
/// Convert to JSON for network transmission (compact format)
|
||||
/// Uses short field names and excludes createdAt to minimize message size
|
||||
/// Sender will be fetched from packet metadata
|
||||
Map<String, dynamic> toNetworkJson();
|
||||
|
||||
/// Parse network JSON (compact format)
|
||||
/// senderName and messageId will be populated from packet metadata
|
||||
static MapDrawing? fromNetworkJson(
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
final typeNum = json['t'] as int?;
|
||||
if (typeNum == null || typeNum < 0 || typeNum >= DrawingShapeType.values.length) {
|
||||
if (typeNum == null ||
|
||||
typeNum < 0 ||
|
||||
typeNum >= DrawingShapeType.values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values[typeNum];
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromNetworkJson(
|
||||
json,
|
||||
senderName: senderName,
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: mapId,
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
static MapDrawing? fromJson(Map<String, dynamic> json) {
|
||||
final typeStr = json['type'] as String?;
|
||||
if (typeStr == null) return null;
|
||||
|
||||
try {
|
||||
final type = DrawingShapeType.values.firstWhere(
|
||||
(e) => e.toString() == 'DrawingShapeType.$typeStr',
|
||||
(value) => value.name == typeStr,
|
||||
);
|
||||
|
||||
switch (type) {
|
||||
case DrawingShapeType.line:
|
||||
return LineDrawing.fromJson(json);
|
||||
case DrawingShapeType.rectangle:
|
||||
return RectangleDrawing.fromJson(json);
|
||||
}
|
||||
} catch (e) {
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the center point of the drawing
|
||||
LatLng getCenter();
|
||||
|
||||
/// Get the bounds of the drawing
|
||||
LatLngBounds getBounds();
|
||||
}
|
||||
|
||||
/// Line drawing on map
|
||||
class LineDrawing extends MapDrawing {
|
||||
final List<LatLng> points;
|
||||
|
||||
@@ -175,6 +165,8 @@ class LineDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.line);
|
||||
|
||||
@override
|
||||
@@ -184,31 +176,51 @@ class LineDrawing extends MapDrawing {
|
||||
'type': type.name,
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'points': points.map((p) => {'lat': p.latitude, 'lon': p.longitude}).toList(),
|
||||
'points': points
|
||||
.map((point) => {'lat': point.latitude, 'lon': point.longitude})
|
||||
.toList(),
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), p=points
|
||||
// Points are encoded as flat array [lat1,lon1,lat2,lon2,...]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'p': points.expand((p) => [
|
||||
double.parse(p.latitude.toStringAsFixed(5)),
|
||||
double.parse(p.longitude.toStringAsFixed(5)),
|
||||
]).toList(),
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['p'] = points
|
||||
.expand((point) => [point.latitude.round(), point.longitude.round()])
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['p'] = points
|
||||
.expand(
|
||||
(point) => [
|
||||
double.parse(point.latitude.toStringAsFixed(5)),
|
||||
double.parse(point.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
)
|
||||
.toList();
|
||||
return payload;
|
||||
}
|
||||
|
||||
static LineDrawing fromJson(Map<String, dynamic> json) {
|
||||
final pointsJson = json['points'] as List<dynamic>;
|
||||
final points = pointsJson.map((p) => LatLng(p['lat'] as double, p['lon'] as double)).toList();
|
||||
final points = pointsJson
|
||||
.map(
|
||||
(point) => LatLng(
|
||||
(point['lat'] as num).toDouble(),
|
||||
(point['lon'] as num).toDouble(),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final senderName = json['sender'] as String?;
|
||||
|
||||
return LineDrawing(
|
||||
@@ -217,8 +229,12 @@ class LineDrawing extends MapDrawing {
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -226,42 +242,60 @@ class LineDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
|
||||
final flatPoints = (json['p'] as List<dynamic>).cast<num>();
|
||||
final points = <LatLng>[];
|
||||
for (int i = 0; i < pointsFlat.length; i += 2) {
|
||||
points.add(LatLng(pointsFlat[i], pointsFlat[i + 1]));
|
||||
for (int i = 0; i < flatPoints.length; i += 2) {
|
||||
points.add(
|
||||
LatLng(flatPoints[i].toDouble(), flatPoints[i + 1].toDouble()),
|
||||
);
|
||||
}
|
||||
|
||||
return LineDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
points: points,
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated points
|
||||
LineDrawing copyWith({List<LatLng>? points}) {
|
||||
LineDrawing copyWith({
|
||||
List<LatLng>? points,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return LineDrawing(
|
||||
id: id,
|
||||
color: color,
|
||||
createdAt: createdAt,
|
||||
points: points ?? this.points,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
if (points.isEmpty) return LatLng(0, 0);
|
||||
if (points.isEmpty) return const LatLng(0, 0);
|
||||
if (points.length == 1) return points[0];
|
||||
|
||||
// Calculate center as average of all points
|
||||
double sumLat = 0;
|
||||
double sumLon = 0;
|
||||
for (final point in points) {
|
||||
@@ -273,8 +307,12 @@ class LineDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
if (points.isEmpty) return LatLngBounds(LatLng(0, 0), LatLng(0, 0));
|
||||
if (points.length == 1) return LatLngBounds(points[0], points[0]);
|
||||
if (points.isEmpty) {
|
||||
return LatLngBounds(const LatLng(0, 0), const LatLng(0, 0));
|
||||
}
|
||||
if (points.length == 1) {
|
||||
return LatLngBounds(points[0], points[0]);
|
||||
}
|
||||
|
||||
double minLat = points[0].latitude;
|
||||
double maxLat = points[0].latitude;
|
||||
@@ -292,7 +330,6 @@ class LineDrawing extends MapDrawing {
|
||||
}
|
||||
}
|
||||
|
||||
/// Rectangle drawing on map
|
||||
class RectangleDrawing extends MapDrawing {
|
||||
final LatLng topLeft;
|
||||
final LatLng bottomRight;
|
||||
@@ -309,16 +346,17 @@ class RectangleDrawing extends MapDrawing {
|
||||
super.isShared,
|
||||
super.isSent,
|
||||
super.isHidden,
|
||||
super.coordinateSpace,
|
||||
super.mapId,
|
||||
}) : super(type: DrawingShapeType.rectangle);
|
||||
|
||||
/// Get all corner points for rendering
|
||||
List<LatLng> get corners => [
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude), // top right
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude), // bottom left
|
||||
topLeft, // close the rectangle
|
||||
];
|
||||
topLeft,
|
||||
LatLng(topLeft.latitude, bottomRight.longitude),
|
||||
bottomRight,
|
||||
LatLng(bottomRight.latitude, topLeft.longitude),
|
||||
topLeft,
|
||||
];
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
@@ -328,27 +366,41 @@ class RectangleDrawing extends MapDrawing {
|
||||
'color': color.toARGB32(),
|
||||
'createdAt': createdAt.toIso8601String(),
|
||||
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
||||
'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude},
|
||||
'bottomRight': {
|
||||
'lat': bottomRight.latitude,
|
||||
'lon': bottomRight.longitude,
|
||||
},
|
||||
'isShared': isShared,
|
||||
// Note: isHidden is not persisted - it's session-only
|
||||
'coordinateSpace': coordinateSpace.name,
|
||||
'mapId': normalizeCustomMapId(mapId),
|
||||
};
|
||||
}
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toNetworkJson() {
|
||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), b=bounds [lat1,lon1,lat2,lon2]
|
||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
||||
// Sender is fetched from packet metadata, not included in JSON
|
||||
return {
|
||||
final payload = <String, dynamic>{
|
||||
't': type.index,
|
||||
'c': DrawingColors.colorToIndex(color),
|
||||
'b': [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
],
|
||||
};
|
||||
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
payload['m'] = normalizeCustomMapId(mapId);
|
||||
payload['b'] = [
|
||||
topLeft.latitude.round(),
|
||||
topLeft.longitude.round(),
|
||||
bottomRight.latitude.round(),
|
||||
bottomRight.longitude.round(),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
payload['b'] = [
|
||||
double.parse(topLeft.latitude.toStringAsFixed(5)),
|
||||
double.parse(topLeft.longitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.latitude.toStringAsFixed(5)),
|
||||
double.parse(bottomRight.longitude.toStringAsFixed(5)),
|
||||
];
|
||||
return payload;
|
||||
}
|
||||
|
||||
static RectangleDrawing fromJson(Map<String, dynamic> json) {
|
||||
@@ -360,11 +412,21 @@ class RectangleDrawing extends MapDrawing {
|
||||
id: json['id'] as String,
|
||||
color: Color(json['color'] as int),
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double),
|
||||
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),
|
||||
topLeft: LatLng(
|
||||
(topLeftJson['lat'] as num).toDouble(),
|
||||
(topLeftJson['lon'] as num).toDouble(),
|
||||
),
|
||||
bottomRight: LatLng(
|
||||
(bottomRightJson['lat'] as num).toDouble(),
|
||||
(bottomRightJson['lon'] as num).toDouble(),
|
||||
),
|
||||
senderName: senderName,
|
||||
isReceived: senderName != null, // Mark as received if sender is present
|
||||
isReceived: senderName != null,
|
||||
isShared: json['isShared'] as bool? ?? false,
|
||||
coordinateSpace: MapCoordinateSpace.fromName(
|
||||
json['coordinateSpace'] as String?,
|
||||
),
|
||||
mapId: normalizeCustomMapId(json['mapId'] as String?),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -372,27 +434,35 @@ class RectangleDrawing extends MapDrawing {
|
||||
Map<String, dynamic> json, {
|
||||
String? senderName,
|
||||
String? messageId,
|
||||
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||
String? mapId,
|
||||
}) {
|
||||
// Parse ultra-compact format
|
||||
final bounds = (json['b'] as List<dynamic>).cast<double>();
|
||||
final bounds = (json['b'] as List<dynamic>).cast<num>();
|
||||
|
||||
return RectangleDrawing(
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
||||
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||
color: DrawingColors.indexToColor(json['c'] as int),
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: LatLng(bounds[0], bounds[1]),
|
||||
bottomRight: LatLng(bounds[2], bounds[3]),
|
||||
topLeft: LatLng(bounds[0].toDouble(), bounds[1].toDouble()),
|
||||
bottomRight: LatLng(bounds[2].toDouble(), bounds[3].toDouble()),
|
||||
senderName: senderName,
|
||||
isReceived: true,
|
||||
messageId: messageId, // Link to source message
|
||||
isShared: false, // Received drawings are not marked as shared
|
||||
messageId: messageId,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: normalizeCustomMapId(mapId),
|
||||
);
|
||||
}
|
||||
|
||||
/// Create a copy with updated corners
|
||||
RectangleDrawing copyWith({
|
||||
LatLng? topLeft,
|
||||
LatLng? bottomRight,
|
||||
bool? isHidden,
|
||||
bool? isShared,
|
||||
bool? isReceived,
|
||||
String? messageId,
|
||||
String? senderName,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return RectangleDrawing(
|
||||
id: id,
|
||||
@@ -400,12 +470,18 @@ class RectangleDrawing extends MapDrawing {
|
||||
createdAt: createdAt,
|
||||
topLeft: topLeft ?? this.topLeft,
|
||||
bottomRight: bottomRight ?? this.bottomRight,
|
||||
isHidden: isHidden ?? this.isHidden,
|
||||
isShared: isShared ?? this.isShared,
|
||||
isReceived: isReceived ?? this.isReceived,
|
||||
messageId: messageId ?? this.messageId,
|
||||
senderName: senderName ?? this.senderName,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
LatLng getCenter() {
|
||||
// Center is the midpoint between top-left and bottom-right
|
||||
return LatLng(
|
||||
(topLeft.latitude + bottomRight.latitude) / 2,
|
||||
(topLeft.longitude + bottomRight.longitude) / 2,
|
||||
@@ -414,7 +490,6 @@ class RectangleDrawing extends MapDrawing {
|
||||
|
||||
@override
|
||||
LatLngBounds getBounds() {
|
||||
// Bounds are simply the two corners
|
||||
return LatLngBounds(topLeft, bottomRight);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export 'package:meshcore_client/meshcore_client.dart'
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'sar_marker.dart';
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
|
||||
extension MessageVoiceExtension on Message {
|
||||
@@ -47,7 +48,7 @@ extension MessageSarExtension on Message {
|
||||
|
||||
/// Convert to a [SarMarker] if this message contains SAR data.
|
||||
SarMarker? toSarMarker() {
|
||||
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
||||
if (!isSarMarker || sarMarkerType == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -57,16 +58,26 @@ extension MessageSarExtension on Message {
|
||||
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
||||
debugPrint(' message.sarCustomEmoji: "$sarCustomEmoji"');
|
||||
|
||||
final coordinateSpace = sarCustomMapPoint != null && sarCustomMapId != null
|
||||
? MapCoordinateSpace.customMap
|
||||
: MapCoordinateSpace.geo;
|
||||
final location = sarCustomMapPoint ?? sarGpsCoordinates;
|
||||
if (location == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return SarMarker(
|
||||
id: id,
|
||||
type: sarMarkerType!,
|
||||
location: sarGpsCoordinates!,
|
||||
location: location,
|
||||
timestamp: sentAt,
|
||||
senderPublicKey: senderPublicKeyPrefix,
|
||||
senderName: senderName,
|
||||
notes: sarNotes,
|
||||
customEmoji: sarCustomEmoji,
|
||||
colorIndex: sarColorIndex,
|
||||
coordinateSpace: coordinateSpace,
|
||||
mapId: sarCustomMapId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ class MessageReceptionDetails {
|
||||
final int? senderToReceiptMs;
|
||||
final int? estimatedTransmitMs;
|
||||
final int? postTransmitDelayMs;
|
||||
final int receivedCopies;
|
||||
|
||||
const MessageReceptionDetails({
|
||||
required this.capturedAt,
|
||||
@@ -40,6 +41,7 @@ class MessageReceptionDetails {
|
||||
this.senderToReceiptMs,
|
||||
this.estimatedTransmitMs,
|
||||
this.postTransmitDelayMs,
|
||||
this.receivedCopies = 1,
|
||||
});
|
||||
|
||||
String? get pathBytesHex => pathBytes == null || pathBytes!.isEmpty
|
||||
@@ -56,9 +58,60 @@ class MessageReceptionDetails {
|
||||
'senderToReceiptMs': senderToReceiptMs,
|
||||
'estimatedTransmitMs': estimatedTransmitMs,
|
||||
'postTransmitDelayMs': postTransmitDelayMs,
|
||||
'receivedCopies': receivedCopies,
|
||||
};
|
||||
}
|
||||
|
||||
MessageReceptionDetails copyWith({
|
||||
DateTime? capturedAt,
|
||||
DateTime? packetLoggedAt,
|
||||
int? rssiDbm,
|
||||
double? snrDb,
|
||||
List<int>? pathBytes,
|
||||
int? senderToReceiptMs,
|
||||
int? estimatedTransmitMs,
|
||||
int? postTransmitDelayMs,
|
||||
int? receivedCopies,
|
||||
}) {
|
||||
return MessageReceptionDetails(
|
||||
capturedAt: capturedAt ?? this.capturedAt,
|
||||
packetLoggedAt: packetLoggedAt ?? this.packetLoggedAt,
|
||||
rssiDbm: rssiDbm ?? this.rssiDbm,
|
||||
snrDb: snrDb ?? this.snrDb,
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
senderToReceiptMs: senderToReceiptMs ?? this.senderToReceiptMs,
|
||||
estimatedTransmitMs: estimatedTransmitMs ?? this.estimatedTransmitMs,
|
||||
postTransmitDelayMs: postTransmitDelayMs ?? this.postTransmitDelayMs,
|
||||
receivedCopies: receivedCopies ?? this.receivedCopies,
|
||||
);
|
||||
}
|
||||
|
||||
static MessageReceptionDetails mergeDuplicate({
|
||||
MessageReceptionDetails? existing,
|
||||
MessageReceptionDetails? incoming,
|
||||
}) {
|
||||
final base =
|
||||
existing ??
|
||||
incoming ??
|
||||
MessageReceptionDetails(capturedAt: DateTime.now());
|
||||
|
||||
return base.copyWith(
|
||||
capturedAt: incoming?.capturedAt ?? existing?.capturedAt,
|
||||
packetLoggedAt: incoming?.packetLoggedAt ?? existing?.packetLoggedAt,
|
||||
rssiDbm: incoming?.rssiDbm ?? existing?.rssiDbm,
|
||||
snrDb: incoming?.snrDb ?? existing?.snrDb,
|
||||
pathBytes: incoming?.pathBytes ?? existing?.pathBytes,
|
||||
senderToReceiptMs:
|
||||
incoming?.senderToReceiptMs ?? existing?.senderToReceiptMs,
|
||||
estimatedTransmitMs:
|
||||
incoming?.estimatedTransmitMs ?? existing?.estimatedTransmitMs,
|
||||
postTransmitDelayMs:
|
||||
incoming?.postTransmitDelayMs ?? existing?.postTransmitDelayMs,
|
||||
receivedCopies:
|
||||
(existing?.receivedCopies ?? 1) + (incoming?.receivedCopies ?? 1),
|
||||
);
|
||||
}
|
||||
|
||||
static MessageReceptionDetails? fromJson(Map<String, dynamic> json) {
|
||||
final capturedAtMillis = json['capturedAtMillis'];
|
||||
if (capturedAtMillis is! int) {
|
||||
@@ -81,6 +134,7 @@ class MessageReceptionDetails {
|
||||
senderToReceiptMs: json['senderToReceiptMs'] as int?,
|
||||
estimatedTransmitMs: json['estimatedTransmitMs'] as int?,
|
||||
postTransmitDelayMs: json['postTransmitDelayMs'] as int?,
|
||||
receivedCopies: json['receivedCopies'] as int? ?? 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
enum PathRecordSource { learned, observed }
|
||||
|
||||
class PathRecord {
|
||||
final List<int> pathBytes;
|
||||
final int hopCount;
|
||||
final int hashSize;
|
||||
final PathRecordSource source;
|
||||
final int successCount;
|
||||
final int failureCount;
|
||||
final int lastRoundTripTimeMs;
|
||||
@@ -11,6 +14,7 @@ class PathRecord {
|
||||
required this.pathBytes,
|
||||
required this.hopCount,
|
||||
required this.hashSize,
|
||||
required this.source,
|
||||
required this.successCount,
|
||||
required this.failureCount,
|
||||
required this.lastRoundTripTimeMs,
|
||||
@@ -27,6 +31,7 @@ class PathRecord {
|
||||
List<int>? pathBytes,
|
||||
int? hopCount,
|
||||
int? hashSize,
|
||||
PathRecordSource? source,
|
||||
int? successCount,
|
||||
int? failureCount,
|
||||
int? lastRoundTripTimeMs,
|
||||
@@ -36,6 +41,7 @@ class PathRecord {
|
||||
pathBytes: pathBytes ?? this.pathBytes,
|
||||
hopCount: hopCount ?? this.hopCount,
|
||||
hashSize: hashSize ?? this.hashSize,
|
||||
source: source ?? this.source,
|
||||
successCount: successCount ?? this.successCount,
|
||||
failureCount: failureCount ?? this.failureCount,
|
||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
||||
@@ -48,6 +54,7 @@ class PathRecord {
|
||||
'path_bytes': pathBytes,
|
||||
'hop_count': hopCount,
|
||||
'hash_size': hashSize,
|
||||
'source': source.name,
|
||||
'success_count': successCount,
|
||||
'failure_count': failureCount,
|
||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
||||
@@ -62,6 +69,10 @@ class PathRecord {
|
||||
.toList(),
|
||||
hopCount: json['hop_count'] as int? ?? 0,
|
||||
hashSize: json['hash_size'] as int? ?? 1,
|
||||
source: PathRecordSource.values.firstWhere(
|
||||
(value) => value.name == (json['source'] as String? ?? 'learned'),
|
||||
orElse: () => PathRecordSource.learned,
|
||||
),
|
||||
successCount: json['success_count'] as int? ?? 0,
|
||||
failureCount: json['failure_count'] as int? ?? 0,
|
||||
lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0,
|
||||
@@ -163,6 +174,10 @@ class ContactPathHistory {
|
||||
};
|
||||
}
|
||||
|
||||
List<PathRecord> get observedPaths => directPaths
|
||||
.where((record) => record.source == PathRecordSource.observed)
|
||||
.toList();
|
||||
|
||||
factory ContactPathHistory.fromJson(
|
||||
String contactPublicKeyHex,
|
||||
Map<String, dynamic> json,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import 'map_coordinate_space.dart';
|
||||
import '../services/sar_template_service.dart';
|
||||
|
||||
/// SAR (Search & Rescue) marker types
|
||||
@@ -78,6 +79,8 @@ class SarMarker {
|
||||
final String? notes;
|
||||
final String? customEmoji; // For custom SAR markers not in predefined types
|
||||
final int? colorIndex; // Color index (0-7) from standard palette
|
||||
final MapCoordinateSpace coordinateSpace;
|
||||
final String? mapId;
|
||||
|
||||
SarMarker({
|
||||
required this.id,
|
||||
@@ -89,8 +92,13 @@ class SarMarker {
|
||||
this.notes,
|
||||
this.customEmoji,
|
||||
this.colorIndex,
|
||||
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||
this.mapId,
|
||||
});
|
||||
|
||||
bool get isCustomMapMarker =>
|
||||
coordinateSpace == MapCoordinateSpace.customMap && mapId != null;
|
||||
|
||||
/// Get sender public key as hex string (short)
|
||||
String? get senderKeyShort {
|
||||
if (senderPublicKey == null || senderPublicKey!.length < 8) return null;
|
||||
@@ -167,6 +175,8 @@ class SarMarker {
|
||||
String? notes,
|
||||
String? customEmoji,
|
||||
int? colorIndex,
|
||||
MapCoordinateSpace? coordinateSpace,
|
||||
String? mapId,
|
||||
}) {
|
||||
return SarMarker(
|
||||
id: id ?? this.id,
|
||||
@@ -178,6 +188,8 @@ class SarMarker {
|
||||
notes: notes ?? this.notes,
|
||||
customEmoji: customEmoji ?? this.customEmoji,
|
||||
colorIndex: colorIndex ?? this.colorIndex,
|
||||
coordinateSpace: coordinateSpace ?? this.coordinateSpace,
|
||||
mapId: mapId ?? this.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
/// SSE Server Configuration Model
|
||||
///
|
||||
/// Configuration for the SSE (Server-Sent Events) web server that enables
|
||||
/// multiple app instances to share a single MeshCore BLE device.
|
||||
class SseServerConfig {
|
||||
/// Server bind address (e.g., "0.0.0.0" for all interfaces, "127.0.0.1" for localhost)
|
||||
final String host;
|
||||
|
||||
/// Server port (default: 12929)
|
||||
final int port;
|
||||
|
||||
/// Whether the SSE server is enabled
|
||||
final bool enabled;
|
||||
|
||||
/// Optional authentication token for basic security
|
||||
/// Clients must include this token in Authorization header
|
||||
final String? authToken;
|
||||
|
||||
const SseServerConfig({
|
||||
this.host = '0.0.0.0',
|
||||
this.port = 12929,
|
||||
this.enabled = false,
|
||||
this.authToken,
|
||||
});
|
||||
|
||||
/// Create a copy with updated fields
|
||||
SseServerConfig copyWith({
|
||||
String? host,
|
||||
int? port,
|
||||
bool? enabled,
|
||||
String? authToken,
|
||||
}) {
|
||||
return SseServerConfig(
|
||||
host: host ?? this.host,
|
||||
port: port ?? this.port,
|
||||
enabled: enabled ?? this.enabled,
|
||||
authToken: authToken ?? this.authToken,
|
||||
);
|
||||
}
|
||||
|
||||
/// Get server URL for clients to connect to
|
||||
String getServerUrl({String? ipAddress}) {
|
||||
final ip = ipAddress ?? host;
|
||||
return 'http://$ip:$port';
|
||||
}
|
||||
|
||||
/// Convert to JSON for persistence
|
||||
Map<String, dynamic> toJson() {
|
||||
return {
|
||||
'host': host,
|
||||
'port': port,
|
||||
'enabled': enabled,
|
||||
'authToken': authToken,
|
||||
};
|
||||
}
|
||||
|
||||
/// Create from JSON
|
||||
factory SseServerConfig.fromJson(Map<String, dynamic> json) {
|
||||
return SseServerConfig(
|
||||
host: json['host'] as String? ?? '0.0.0.0',
|
||||
port: json['port'] as int? ?? 12929,
|
||||
enabled: json['enabled'] as bool? ?? false,
|
||||
authToken: json['authToken'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SseServerConfig(host: $host, port: $port, enabled: $enabled, hasAuth: ${authToken != null})';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,11 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/drawing_message_parser.dart';
|
||||
|
||||
/// Drawing mode state
|
||||
@@ -11,6 +14,8 @@ enum DrawingMode { none, line, rectangle, measure }
|
||||
/// Provider for managing map drawings
|
||||
class DrawingProvider with ChangeNotifier {
|
||||
static const String _storageKey = 'map_drawings';
|
||||
static const String _showReceivedDrawingsKey = 'map_show_received_drawings';
|
||||
static const String _showSarMarkersKey = 'map_show_sar_markers';
|
||||
|
||||
// Drawing state
|
||||
DrawingMode _drawingMode = DrawingMode.none;
|
||||
@@ -20,6 +25,7 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
// Completed drawings
|
||||
final List<MapDrawing> _drawings = [];
|
||||
bool _isInitialized = false;
|
||||
|
||||
// In-progress drawing
|
||||
MapDrawing? _currentDrawing;
|
||||
@@ -30,12 +36,18 @@ class DrawingProvider with ChangeNotifier {
|
||||
LatLng? _measurementPoint1;
|
||||
LatLng? _measurementPoint2;
|
||||
double? _measuredDistance; // in meters
|
||||
MapCoordinateSpace _activeCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _activeMapId;
|
||||
double? _activeMetersPerPixel;
|
||||
|
||||
// Getters
|
||||
DrawingMode get drawingMode => _drawingMode;
|
||||
Color get selectedColor => _selectedColor;
|
||||
bool get showReceivedDrawings => _showReceivedDrawings;
|
||||
bool get showSarMarkers => _showSarMarkers;
|
||||
MapCoordinateSpace get activeCoordinateSpace => _activeCoordinateSpace;
|
||||
String? get activeMapId => _activeMapId;
|
||||
double? get activeMetersPerPixel => _activeMetersPerPixel;
|
||||
List<MapDrawing> get drawings {
|
||||
// Filter out hidden drawings first
|
||||
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
||||
@@ -45,19 +57,62 @@ class DrawingProvider with ChangeNotifier {
|
||||
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
|
||||
}
|
||||
|
||||
visibleDrawings = visibleDrawings.where((drawing) {
|
||||
if (drawing.coordinateSpace != _activeCoordinateSpace) {
|
||||
return false;
|
||||
}
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return drawing.mapId != null && drawing.mapId == _activeMapId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
return List.unmodifiable(visibleDrawings.toList());
|
||||
}
|
||||
|
||||
MapDrawing? get currentDrawing => _currentDrawing;
|
||||
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
||||
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
||||
bool get isDrawing => _drawingMode != DrawingMode.none;
|
||||
bool get isInitialized => _isInitialized;
|
||||
LatLng? get measurementPoint1 => _measurementPoint1;
|
||||
LatLng? get measurementPoint2 => _measurementPoint2;
|
||||
double? get measuredDistance => _measuredDistance;
|
||||
|
||||
/// Initialize and load saved drawings
|
||||
Future<void> initialize() async {
|
||||
await _loadPreferences();
|
||||
await _loadDrawings();
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _loadPreferences();
|
||||
await _loadDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void setMapContext({
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
double? metersPerPixel,
|
||||
}) {
|
||||
final changed =
|
||||
coordinateSpace != _activeCoordinateSpace ||
|
||||
mapId != _activeMapId ||
|
||||
metersPerPixel != _activeMetersPerPixel;
|
||||
_activeCoordinateSpace = coordinateSpace;
|
||||
_activeMapId = mapId;
|
||||
_activeMetersPerPixel = metersPerPixel;
|
||||
if (_measurementPoint1 != null && _measurementPoint2 != null) {
|
||||
_measuredDistance = _calculateDistance(
|
||||
_measurementPoint1!,
|
||||
_measurementPoint2!,
|
||||
);
|
||||
}
|
||||
if (changed) {
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
|
||||
/// Set drawing mode
|
||||
@@ -77,15 +132,34 @@ class DrawingProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
/// Toggle visibility of received drawings
|
||||
void toggleReceivedDrawings() {
|
||||
Future<void> toggleReceivedDrawings() async {
|
||||
_showReceivedDrawings = !_showReceivedDrawings;
|
||||
notifyListeners();
|
||||
await _savePreferences();
|
||||
}
|
||||
|
||||
/// Toggle visibility of SAR markers
|
||||
void toggleSarMarkers() {
|
||||
Future<void> toggleSarMarkers() async {
|
||||
_showSarMarkers = !_showSarMarkers;
|
||||
notifyListeners();
|
||||
await _savePreferences();
|
||||
}
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showReceivedDrawings =
|
||||
prefs.getBool(_scopedKey(_showReceivedDrawingsKey)) ?? true;
|
||||
_showSarMarkers = prefs.getBool(_scopedKey(_showSarMarkersKey)) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _savePreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
_scopedKey(_showReceivedDrawingsKey),
|
||||
_showReceivedDrawings,
|
||||
);
|
||||
await prefs.setBool(_scopedKey(_showSarMarkersKey), _showSarMarkers);
|
||||
}
|
||||
|
||||
/// Start drawing a line
|
||||
@@ -116,6 +190,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: List.from(_currentLinePoints),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -159,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
? _rectangleStartPoint!.longitude
|
||||
: endPoint.longitude,
|
||||
),
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -195,6 +277,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
createdAt: DateTime.now(),
|
||||
topLeft: topLeft,
|
||||
bottomRight: bottomRight,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
|
||||
_drawings.add(drawing);
|
||||
@@ -216,7 +302,9 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Set second measurement point and calculate distance
|
||||
void setMeasurementPoint2(LatLng point) {
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
|
||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_measurementPoint2 = point;
|
||||
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
|
||||
@@ -225,6 +313,13 @@ class DrawingProvider with ChangeNotifier {
|
||||
|
||||
/// Calculate distance between two points using Haversine formula
|
||||
double _calculateDistance(LatLng point1, LatLng point2) {
|
||||
if (_activeCoordinateSpace == MapCoordinateSpace.customMap &&
|
||||
_activeMetersPerPixel != null) {
|
||||
final dy = point2.latitude - point1.latitude;
|
||||
final dx = point2.longitude - point1.longitude;
|
||||
final pixelDistance = math.sqrt((dx * dx) + (dy * dy));
|
||||
return pixelDistance * _activeMetersPerPixel!;
|
||||
}
|
||||
const Distance distance = Distance();
|
||||
return distance.as(LengthUnit.Meter, point1, point2);
|
||||
}
|
||||
@@ -281,7 +376,7 @@ class DrawingProvider with ChangeNotifier {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonList = _drawings.map((d) => d.toJson()).toList();
|
||||
final jsonString = jsonEncode(jsonList);
|
||||
await prefs.setString(_storageKey, jsonString);
|
||||
await prefs.setString(_scopedKey(_storageKey), jsonString);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving drawings: $e');
|
||||
}
|
||||
@@ -291,8 +386,11 @@ class DrawingProvider with ChangeNotifier {
|
||||
Future<void> _loadDrawings() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_storageKey);
|
||||
if (jsonString == null) return;
|
||||
final jsonString = prefs.getString(_scopedKey(_storageKey));
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
_drawings.clear();
|
||||
return;
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
_drawings.clear();
|
||||
@@ -318,6 +416,10 @@ class DrawingProvider with ChangeNotifier {
|
||||
color: _selectedColor,
|
||||
createdAt: DateTime.now(),
|
||||
points: _currentLinePoints,
|
||||
coordinateSpace: _activeCoordinateSpace,
|
||||
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||
? _activeMapId
|
||||
: null,
|
||||
);
|
||||
} else if (_drawingMode == DrawingMode.rectangle &&
|
||||
_currentDrawing != null) {
|
||||
@@ -355,6 +457,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
return RectangleDrawing(
|
||||
@@ -369,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
return drawing;
|
||||
@@ -383,9 +489,27 @@ class DrawingProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
List<Map<String, dynamic>> exportDrawingsJson() {
|
||||
return _drawings.map((drawing) => drawing.toJson()).toList();
|
||||
}
|
||||
|
||||
Future<void> replaceDrawingsFromJson(
|
||||
List<Map<String, dynamic>> jsonList,
|
||||
) async {
|
||||
_drawings
|
||||
..clear()
|
||||
..addAll(jsonList.map(MapDrawing.fromJson).whereType<MapDrawing>());
|
||||
await _saveDrawings();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
/// Get all unshared drawings (local drawings not yet sent)
|
||||
List<MapDrawing> getUnsharedDrawings() {
|
||||
return _drawings.where((d) => !d.isShared && !d.isReceived).toList();
|
||||
return drawings.where((d) => !d.isShared && !d.isReceived).toList();
|
||||
}
|
||||
|
||||
/// Mark a drawing as shared
|
||||
@@ -407,6 +531,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -421,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: true,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -448,6 +576,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
} else if (drawing is RectangleDrawing) {
|
||||
_drawings[index] = RectangleDrawing(
|
||||
@@ -462,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
|
||||
isShared: drawing.isShared,
|
||||
isSent: drawing.isSent,
|
||||
isHidden: !drawing.isHidden,
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
/// Message delivery tracking helper
|
||||
@@ -30,6 +31,9 @@ class MessageDeliveryTracker {
|
||||
/// Map of ACK tag to timestamp for timeout cleanup
|
||||
final Map<int, DateTime> _ackTagTimestamps = {};
|
||||
|
||||
/// Completer signalled when a pending ACK slot is freed (delivery or removal).
|
||||
Completer<void>? _slotFreedCompleter;
|
||||
|
||||
/// Track a pending message ID before sending
|
||||
///
|
||||
/// This is called BEFORE sending the message. When RESP_CODE_SENT
|
||||
@@ -112,6 +116,7 @@ class MessageDeliveryTracker {
|
||||
_messageIdToAckTag.remove(messageId);
|
||||
}
|
||||
_ackTagTimestamps.remove(ackCode);
|
||||
_notifySlotFreed();
|
||||
}
|
||||
|
||||
/// Remove ACK tag mapping by message ID
|
||||
@@ -122,6 +127,7 @@ class MessageDeliveryTracker {
|
||||
if (ackTag != null) {
|
||||
_ackTagToMessageId.remove(ackTag);
|
||||
_ackTagTimestamps.remove(ackTag);
|
||||
_notifySlotFreed();
|
||||
}
|
||||
_pendingMessageIds.remove(messageId);
|
||||
final emptyKeys = <String>[];
|
||||
@@ -166,6 +172,7 @@ class MessageDeliveryTracker {
|
||||
_ackTagToMessageId.clear();
|
||||
_messageIdToAckTag.clear();
|
||||
_ackTagTimestamps.clear();
|
||||
_notifySlotFreed();
|
||||
}
|
||||
|
||||
/// Get count of pending ACK tags
|
||||
@@ -179,6 +186,27 @@ class MessageDeliveryTracker {
|
||||
/// Returns true if >= 7 pending ACKs (stay under firmware limit of 8)
|
||||
bool get shouldRateLimit => pendingCount >= 7;
|
||||
|
||||
/// Wait until a pending ACK slot is freed, or [timeout] elapses.
|
||||
///
|
||||
/// Returns immediately if not at the rate limit.
|
||||
Future<void> waitForSlot({
|
||||
Duration timeout = const Duration(milliseconds: 500),
|
||||
}) async {
|
||||
if (!shouldRateLimit) return;
|
||||
_slotFreedCompleter ??= Completer<void>();
|
||||
await _slotFreedCompleter!.future.timeout(
|
||||
timeout,
|
||||
onTimeout: () {},
|
||||
);
|
||||
}
|
||||
|
||||
void _notifySlotFreed() {
|
||||
if (_slotFreedCompleter != null && !_slotFreedCompleter!.isCompleted) {
|
||||
_slotFreedCompleter!.complete();
|
||||
}
|
||||
_slotFreedCompleter = null;
|
||||
}
|
||||
|
||||
/// Get oldest pending ACK timestamp (for debugging)
|
||||
DateTime? get oldestPendingTimestamp {
|
||||
if (_ackTagTimestamps.isEmpty) return null;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../../models/message.dart';
|
||||
import '../../models/contact.dart';
|
||||
@@ -19,10 +20,19 @@ class MessageRetryManager {
|
||||
final Map<String, DateTime> _lastRetryTimes = {};
|
||||
final Map<String, int> _pathFailureStreaks = {};
|
||||
|
||||
static const int maxRetryAttempts = 4;
|
||||
/// Max retry attempts when the contact has a known path.
|
||||
/// Official MeshCore app uses 5 (with auto-retry) or 3 (without).
|
||||
static const int maxRetryAttemptsWithPath = 5;
|
||||
|
||||
/// No retries for flood-only contacts (no known path).
|
||||
/// Value 0 means: don't retry at all, go straight to fallback/fail.
|
||||
static const int maxRetryAttemptsFloodOnly = 0;
|
||||
|
||||
@Deprecated('Use maxRetryAttemptsForContact instead')
|
||||
static const int maxRetryAttempts = maxRetryAttemptsWithPath;
|
||||
|
||||
// Retry backoff values in milliseconds.
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000];
|
||||
static const List<int> _retryDelays = [1000, 2000, 4000, 8000, 8000];
|
||||
static const int _defaultLoRaSf = 10;
|
||||
static const int _defaultLoRaCr = 5;
|
||||
static const int _defaultLoRaBwHz = 250000;
|
||||
@@ -38,32 +48,57 @@ class MessageRetryManager {
|
||||
return _retryDelays[attempt];
|
||||
}
|
||||
|
||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
||||
/// provide one or returns an invalid value.
|
||||
static final math.Random _rng = math.Random();
|
||||
|
||||
/// Calculate a delivery-ACK timeout with random jitter.
|
||||
///
|
||||
/// Matches the official MeshCore app: `suggestedTimeout + random(1-8s)`.
|
||||
/// The jitter prevents collision when multiple messages are in flight.
|
||||
int calculateAckTimeoutMs({
|
||||
required String text,
|
||||
required Contact? contact,
|
||||
int? suggestedTimeoutMs,
|
||||
}) {
|
||||
int baseTimeout;
|
||||
if (suggestedTimeoutMs != null && suggestedTimeoutMs > 0) {
|
||||
return suggestedTimeoutMs;
|
||||
baseTimeout = suggestedTimeoutMs;
|
||||
} else {
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
|
||||
if (hopCount < 0) {
|
||||
baseTimeout = ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
} else {
|
||||
baseTimeout = ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
}
|
||||
}
|
||||
|
||||
final payloadBytes = utf8.encode(text).length;
|
||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
||||
final hopCount = contact?.routeHasPath == true
|
||||
? contact!.routeHopCount
|
||||
: -1;
|
||||
// Add random jitter: 500-2000ms to avoid collision
|
||||
final jitterMs = 500 + _rng.nextInt(1501);
|
||||
return baseTimeout + jitterMs;
|
||||
}
|
||||
|
||||
if (hopCount < 0) {
|
||||
return ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
||||
}
|
||||
|
||||
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
||||
/// Max attempts for a given contact based on whether it has a known path.
|
||||
static int maxRetryAttemptsForContact(Contact? contact) {
|
||||
final hasPath = contact?.routeHasPath ?? false;
|
||||
return hasPath ? maxRetryAttemptsWithPath : maxRetryAttemptsFloodOnly;
|
||||
}
|
||||
|
||||
bool canRetry(Message message, Contact contact) {
|
||||
return message.retryAttempt < maxRetryAttempts;
|
||||
return message.retryAttempt < maxRetryAttemptsForContact(contact);
|
||||
}
|
||||
|
||||
/// Whether this is the last retry attempt.
|
||||
/// When true, the caller should reset the path to force flood mode.
|
||||
///
|
||||
/// Note: called AFTER retryAttempt has been incremented, so we compare
|
||||
/// directly against maxAttempts (not +1).
|
||||
bool isLastAttempt(Message message, Contact contact) {
|
||||
final maxAttempts = maxRetryAttemptsForContact(contact);
|
||||
return maxAttempts > 1 && message.retryAttempt >= maxAttempts;
|
||||
}
|
||||
|
||||
/// Track a retry attempt for a message
|
||||
|
||||
@@ -38,12 +38,6 @@ Future<bool> serveCachedSessionFragments<T>({
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!requester.routeSupportsLegacyRawTransport) {
|
||||
debugPrint(
|
||||
'⚠️ [$providerLabel] ${requester.advName} route uses unsupported 3-byte raw transport on current client',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (requester.outPath.isEmpty) {
|
||||
debugPrint(
|
||||
'⚠️ [$providerLabel] ${requester.advName} has empty outPath payload',
|
||||
@@ -64,7 +58,7 @@ Future<bool> serveCachedSessionFragments<T>({
|
||||
try {
|
||||
await sendRawPacket(
|
||||
contactPath: requester.outPath,
|
||||
contactPathLen: requester.routeSignedPathLen,
|
||||
contactPathLen: requester.routeEncodedPathLen,
|
||||
payload: encodeBinary(fragment),
|
||||
);
|
||||
servedCount++;
|
||||
|
||||
@@ -1,24 +1,48 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:crypto/crypto.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/custom_map_config.dart';
|
||||
import '../models/location_trail.dart';
|
||||
import '../models/map_coordinate_space.dart';
|
||||
import '../models/map_drawing.dart';
|
||||
import '../models/sar_marker.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../utils/custom_map_id.dart';
|
||||
|
||||
class MapProvider with ChangeNotifier {
|
||||
static const String _customMapConfigKey = 'custom_map_config_v1';
|
||||
static const String _customMapModeKey = 'custom_map_mode_v1';
|
||||
static const String _customMapsDirName = 'custom_maps';
|
||||
|
||||
MapProvider() {
|
||||
unawaited(_loadInitialState());
|
||||
}
|
||||
|
||||
final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
LatLng? _targetLocation;
|
||||
LatLngBounds? _targetBounds;
|
||||
double? _targetZoom;
|
||||
bool _shouldAnimate = false;
|
||||
MapCoordinateSpace _targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
String? _targetMapId;
|
||||
|
||||
// Track which contact paths are currently visible
|
||||
final Set<String> _visibleContactPaths = {};
|
||||
|
||||
// Location trail tracking
|
||||
LocationTrail? _currentTrail;
|
||||
bool _isTrailVisible = true;
|
||||
final List<LocationTrail> _trailHistory = [];
|
||||
|
||||
// WMS overlay toggles
|
||||
bool _showCadastralOverlay = false;
|
||||
bool _showForestRoadsOverlay = false;
|
||||
bool _showHikingTrailsOverlay = false;
|
||||
@@ -31,28 +55,30 @@ class MapProvider with ChangeNotifier {
|
||||
bool _showPlaceNamesOverlay = false;
|
||||
bool _showMunicipalityBordersOverlay = false;
|
||||
|
||||
// Contact trail toggles
|
||||
bool _showAllContactTrails = true; // Default to showing all contact trails
|
||||
bool _showAllContactTrails = true;
|
||||
bool _hideRepeatersOnMap = false;
|
||||
|
||||
// Imported trail (from GPX)
|
||||
LocationTrail? _importedTrail;
|
||||
|
||||
// Download area selection
|
||||
bool _isSelectingDownloadArea = false;
|
||||
LatLngBounds? _downloadAreaBounds;
|
||||
|
||||
CustomMapConfig? _customMapConfig;
|
||||
bool _isUsingCustomMap = false;
|
||||
|
||||
LatLng? get targetLocation => _targetLocation;
|
||||
LatLngBounds? get targetBounds => _targetBounds;
|
||||
double? get targetZoom => _targetZoom;
|
||||
bool get shouldAnimate => _shouldAnimate;
|
||||
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
|
||||
String? get targetMapId => _targetMapId;
|
||||
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
|
||||
|
||||
// Trail getters
|
||||
LocationTrail? get currentTrail => _currentTrail;
|
||||
bool get isTrailVisible => _isTrailVisible;
|
||||
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
||||
bool get isTrailActive => _currentTrail?.isActive ?? false;
|
||||
|
||||
// WMS overlay getters
|
||||
bool get showCadastralOverlay => _showCadastralOverlay;
|
||||
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
||||
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
|
||||
@@ -65,68 +91,139 @@ class MapProvider with ChangeNotifier {
|
||||
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
||||
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
||||
|
||||
// Contact trail getters
|
||||
bool get showAllContactTrails => _showAllContactTrails;
|
||||
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
||||
|
||||
// Imported trail getters
|
||||
LocationTrail? get importedTrail => _importedTrail;
|
||||
|
||||
// Download area getters
|
||||
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
|
||||
LatLngBounds? get downloadAreaBounds => _downloadAreaBounds;
|
||||
|
||||
CustomMapConfig? get customMapConfig => _customMapConfig;
|
||||
bool get hasCustomMap => _customMapConfig != null;
|
||||
bool get isUsingCustomMap => _isUsingCustomMap && _customMapConfig != null;
|
||||
bool get shouldHideGpsData => isUsingCustomMap;
|
||||
|
||||
LatLngBounds? get customMapBounds => _customMapConfig?.bounds;
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
await _loadInitialState();
|
||||
}
|
||||
|
||||
bool matchesActiveCustomMap(String? mapId) {
|
||||
return hasCustomMap &&
|
||||
normalizeCustomMapId(_customMapConfig!.mapId) ==
|
||||
normalizeCustomMapId(mapId);
|
||||
}
|
||||
|
||||
void navigateToLocation({
|
||||
required LatLng location,
|
||||
double zoom = 15.0,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
_targetLocation = location;
|
||||
_targetBounds = null;
|
||||
_targetZoom = zoom;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
_targetMapId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String? navigateToMapPoint({
|
||||
required LatLng point,
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
double zoom = 15.0,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(mapId)) {
|
||||
return 'Load the matching custom map to view this item.';
|
||||
}
|
||||
if (!_isUsingCustomMap) {
|
||||
_isUsingCustomMap = true;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
} else if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
|
||||
_targetLocation = point;
|
||||
_targetBounds = null;
|
||||
_targetZoom = zoom;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = coordinateSpace;
|
||||
_targetMapId = mapId;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
String? navigateToBounds({
|
||||
required LatLngBounds bounds,
|
||||
required MapCoordinateSpace coordinateSpace,
|
||||
String? mapId,
|
||||
bool animate = true,
|
||||
}) {
|
||||
if (coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(mapId)) {
|
||||
return 'Load the matching custom map to view this item.';
|
||||
}
|
||||
if (!_isUsingCustomMap) {
|
||||
_isUsingCustomMap = true;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
} else if (_isUsingCustomMap) {
|
||||
_isUsingCustomMap = false;
|
||||
unawaited(_saveCustomMapState());
|
||||
}
|
||||
|
||||
_targetBounds = bounds;
|
||||
_targetLocation = bounds.center;
|
||||
_targetZoom = null;
|
||||
_shouldAnimate = animate;
|
||||
_targetCoordinateSpace = coordinateSpace;
|
||||
_targetMapId = mapId;
|
||||
notifyListeners();
|
||||
return null;
|
||||
}
|
||||
|
||||
void clearNavigation() {
|
||||
_targetLocation = null;
|
||||
_targetBounds = null;
|
||||
_targetZoom = null;
|
||||
_shouldAnimate = false;
|
||||
// Don't notify listeners to avoid rebuilds
|
||||
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||
_targetMapId = null;
|
||||
}
|
||||
|
||||
/// Navigate to a drawing by its ID
|
||||
void navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||
debugPrint('🗺️ [MapProvider] navigateToDrawing called with ID: $drawingId');
|
||||
// Find the drawing in the provider
|
||||
final drawings = drawingProvider.drawings as List;
|
||||
debugPrint('🗺️ [MapProvider] Total drawings in provider: ${drawings.length}');
|
||||
final drawing = drawings.cast<dynamic>().firstWhere(
|
||||
(d) => d.id == drawingId,
|
||||
orElse: () => null,
|
||||
);
|
||||
|
||||
String? navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||
final drawing = drawingProvider.getDrawingById(drawingId) as MapDrawing?;
|
||||
if (drawing == null) {
|
||||
debugPrint('⚠️ [MapProvider] Drawing $drawingId not found');
|
||||
debugPrint('⚠️ [MapProvider] Available drawing IDs: ${drawings.map((d) => d.id).toList()}');
|
||||
return;
|
||||
return 'Drawing not found.';
|
||||
}
|
||||
|
||||
// Use MapDrawing's built-in getCenter and getBounds methods
|
||||
final center = drawing.getCenter();
|
||||
final bounds = drawing.getBounds();
|
||||
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
if (!matchesActiveCustomMap(drawing.mapId)) {
|
||||
return 'Load the matching custom map to view this drawing.';
|
||||
}
|
||||
return navigateToBounds(
|
||||
bounds: drawing.getBounds(),
|
||||
coordinateSpace: drawing.coordinateSpace,
|
||||
mapId: drawing.mapId,
|
||||
);
|
||||
}
|
||||
|
||||
// Calculate appropriate zoom level based on bounds
|
||||
// For larger drawings, use lower zoom to fit the whole drawing
|
||||
// For smaller drawings, use higher zoom for better detail
|
||||
final bounds = drawing.getBounds();
|
||||
final latDiff = (bounds.north - bounds.south).abs();
|
||||
final lonDiff = (bounds.east - bounds.west).abs();
|
||||
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
|
||||
|
||||
// Zoom scale: smaller drawings get higher zoom
|
||||
// 0.001 degrees (~100m) -> zoom 17
|
||||
// 0.005 degrees (~500m) -> zoom 16
|
||||
// 0.01 degrees (~1km) -> zoom 15
|
||||
// 0.05 degrees (~5km) -> zoom 13
|
||||
// 0.1 degrees (~10km) -> zoom 12
|
||||
double zoom = 15.0;
|
||||
if (maxDiff < 0.001) {
|
||||
zoom = 17.0;
|
||||
@@ -142,9 +239,125 @@ class MapProvider with ChangeNotifier {
|
||||
zoom = 10.0;
|
||||
}
|
||||
|
||||
final typeStr = drawing is LineDrawing ? 'line' : 'rectangle';
|
||||
debugPrint('🗺️ [MapProvider] Navigating to drawing: $typeStr, zoom: $zoom');
|
||||
navigateToLocation(location: center, zoom: zoom, animate: true);
|
||||
navigateToLocation(
|
||||
location: drawing.getCenter(),
|
||||
zoom: zoom,
|
||||
animate: true,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
String? navigateToSarMarker(SarMarker marker) {
|
||||
if (marker.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||
return navigateToMapPoint(
|
||||
point: marker.location,
|
||||
coordinateSpace: MapCoordinateSpace.customMap,
|
||||
mapId: marker.mapId,
|
||||
);
|
||||
}
|
||||
navigateToLocation(location: marker.location, zoom: 15.0, animate: true);
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<bool> loadCustomMapFromGallery() async {
|
||||
final picked = await _imagePicker.pickImage(source: ImageSource.gallery);
|
||||
if (picked == null) {
|
||||
return false;
|
||||
}
|
||||
final bytes = await picked.readAsBytes();
|
||||
await setCustomMapImage(bytes: bytes, displayName: picked.name);
|
||||
return true;
|
||||
}
|
||||
|
||||
Future<void> replaceCustomMap() async {
|
||||
await loadCustomMapFromGallery();
|
||||
}
|
||||
|
||||
Future<void> setCustomMapImage({
|
||||
required Uint8List bytes,
|
||||
required String displayName,
|
||||
}) async {
|
||||
final dimensions = await _decodeImageSize(bytes);
|
||||
final mapId = normalizeCustomMapId(sha256.convert(bytes).toString())!;
|
||||
final documentsDir = await getApplicationDocumentsDirectory();
|
||||
final dir = Directory('${documentsDir.path}/$_customMapsDirName');
|
||||
if (!await dir.exists()) {
|
||||
await dir.create(recursive: true);
|
||||
}
|
||||
|
||||
final extension = _normalizedExtension(displayName);
|
||||
final nextPath = '${dir.path}/custom_map_$mapId.$extension';
|
||||
final nextFile = File(nextPath);
|
||||
await nextFile.writeAsBytes(bytes, flush: true);
|
||||
|
||||
final previousPath = _customMapConfig?.filePath;
|
||||
_customMapConfig = CustomMapConfig(
|
||||
filePath: nextPath,
|
||||
displayName: displayName,
|
||||
mapId: mapId,
|
||||
imageWidth: dimensions.$1,
|
||||
imageHeight: dimensions.$2,
|
||||
);
|
||||
_isUsingCustomMap = true;
|
||||
await _saveCustomMapState();
|
||||
|
||||
if (previousPath != null && previousPath != nextPath) {
|
||||
unawaited(_deleteFileIfExists(previousPath));
|
||||
}
|
||||
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setCustomMapCalibration({
|
||||
required LatLng pointA,
|
||||
required LatLng pointB,
|
||||
required double metersPerPixel,
|
||||
}) async {
|
||||
if (_customMapConfig == null) return;
|
||||
_customMapConfig = _customMapConfig!.copyWith(
|
||||
calibrationPointA: pointA,
|
||||
calibrationPointB: pointB,
|
||||
metersPerPixel: metersPerPixel,
|
||||
);
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> clearCustomMapCalibration() async {
|
||||
if (_customMapConfig == null) return;
|
||||
_customMapConfig = _customMapConfig!.copyWith(
|
||||
clearMetersPerPixel: true,
|
||||
clearCalibrationPointA: true,
|
||||
clearCalibrationPointB: true,
|
||||
);
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> removeCustomMap() async {
|
||||
final filePath = _customMapConfig?.filePath;
|
||||
_customMapConfig = null;
|
||||
_isUsingCustomMap = false;
|
||||
clearNavigation();
|
||||
await _saveCustomMapState();
|
||||
if (filePath != null) {
|
||||
await _deleteFileIfExists(filePath);
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> enterCustomMapMode() async {
|
||||
if (!hasCustomMap || _isUsingCustomMap) return;
|
||||
_isUsingCustomMap = true;
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> exitCustomMapMode() async {
|
||||
if (!_isUsingCustomMap) return;
|
||||
_isUsingCustomMap = false;
|
||||
await _saveCustomMapState();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
void updateZoom(double zoom) {
|
||||
@@ -152,7 +365,6 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Toggle path visibility for a contact
|
||||
void toggleContactPath(String publicKeyHex) {
|
||||
if (_visibleContactPaths.contains(publicKeyHex)) {
|
||||
_visibleContactPaths.remove(publicKeyHex);
|
||||
@@ -162,27 +374,22 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Check if a contact's path is visible
|
||||
bool isContactPathVisible(String publicKeyHex) {
|
||||
return _visibleContactPaths.contains(publicKeyHex);
|
||||
}
|
||||
|
||||
/// Hide all contact paths
|
||||
void hideAllPaths() {
|
||||
_visibleContactPaths.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Show path for specific contact (hide all others)
|
||||
void showOnlyPath(String publicKeyHex) {
|
||||
_visibleContactPaths.clear();
|
||||
_visibleContactPaths.add(publicKeyHex);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Start a new location trail
|
||||
void startTrail() {
|
||||
// End current trail if active
|
||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||
endTrail();
|
||||
}
|
||||
@@ -195,22 +402,22 @@ class MapProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Add a point to the current trail
|
||||
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
|
||||
if (_currentTrail == null || !_currentTrail!.isActive) {
|
||||
startTrail();
|
||||
}
|
||||
|
||||
_currentTrail!.addPoint(TrailPoint(
|
||||
position: position,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: accuracy,
|
||||
speed: speed,
|
||||
));
|
||||
_currentTrail!.addPoint(
|
||||
TrailPoint(
|
||||
position: position,
|
||||
timestamp: DateTime.now(),
|
||||
accuracy: accuracy,
|
||||
speed: speed,
|
||||
),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// End the current trail
|
||||
void endTrail() {
|
||||
if (_currentTrail != null) {
|
||||
_currentTrail!.isActive = false;
|
||||
@@ -223,13 +430,11 @@ class MapProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Toggle trail visibility
|
||||
void toggleTrailVisibility() {
|
||||
_isTrailVisible = !_isTrailVisible;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear the current trail
|
||||
void clearCurrentTrail() {
|
||||
if (_currentTrail != null) {
|
||||
_currentTrail = null;
|
||||
@@ -237,197 +442,362 @@ class MapProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
/// Clear all trail history
|
||||
void clearAllTrails() {
|
||||
_currentTrail = null;
|
||||
_trailHistory.clear();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Get total trail distance in meters
|
||||
double get totalTrailDistance {
|
||||
if (_currentTrail == null) return 0;
|
||||
return _currentTrail!.totalDistance;
|
||||
}
|
||||
|
||||
/// Get trail duration
|
||||
Duration get trailDuration {
|
||||
if (_currentTrail == null) return Duration.zero;
|
||||
return _currentTrail!.duration;
|
||||
}
|
||||
|
||||
/// Toggle cadastral parcels overlay
|
||||
Future<void> toggleCadastralOverlay() async {
|
||||
_showCadastralOverlay = !_showCadastralOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle forest roads overlay
|
||||
Future<void> toggleForestRoadsOverlay() async {
|
||||
_showForestRoadsOverlay = !_showForestRoadsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle hiking trails overlay
|
||||
Future<void> toggleHikingTrailsOverlay() async {
|
||||
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle main roads overlay
|
||||
Future<void> toggleMainRoadsOverlay() async {
|
||||
_showMainRoadsOverlay = !_showMainRoadsOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle house numbers overlay
|
||||
Future<void> toggleHouseNumbersOverlay() async {
|
||||
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle fire hazard zones overlay
|
||||
Future<void> toggleFireHazardZonesOverlay() async {
|
||||
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle historical fires overlay
|
||||
Future<void> toggleHistoricalFiresOverlay() async {
|
||||
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle firebreaks overlay
|
||||
Future<void> toggleFirebreaksOverlay() async {
|
||||
_showFirebreaksOverlay = !_showFirebreaksOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle Kras fire zones overlay
|
||||
Future<void> toggleKrasFireZonesOverlay() async {
|
||||
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle place names overlay
|
||||
Future<void> togglePlaceNamesOverlay() async {
|
||||
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Toggle municipality borders overlay
|
||||
Future<void> toggleMunicipalityBordersOverlay() async {
|
||||
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
|
||||
notifyListeners();
|
||||
await _saveOverlayState();
|
||||
}
|
||||
|
||||
/// Load overlay state from SharedPreferences
|
||||
Future<void> loadOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showCadastralOverlay = prefs.getBool('map_show_cadastral_overlay') ?? false;
|
||||
_showForestRoadsOverlay = prefs.getBool('map_show_forest_roads_overlay') ?? false;
|
||||
_showHikingTrailsOverlay = prefs.getBool('map_show_hiking_trails_overlay') ?? false;
|
||||
_showMainRoadsOverlay = prefs.getBool('map_show_main_roads_overlay') ?? false;
|
||||
_showHouseNumbersOverlay = prefs.getBool('map_show_house_numbers_overlay') ?? false;
|
||||
_showFireHazardZonesOverlay = prefs.getBool('map_show_fire_hazard_zones_overlay') ?? false;
|
||||
_showHistoricalFiresOverlay = prefs.getBool('map_show_historical_fires_overlay') ?? false;
|
||||
_showFirebreaksOverlay = prefs.getBool('map_show_firebreaks_overlay') ?? false;
|
||||
_showKrasFireZonesOverlay = prefs.getBool('map_show_kras_fire_zones_overlay') ?? false;
|
||||
_showPlaceNamesOverlay = prefs.getBool('map_show_place_names_overlay') ?? false;
|
||||
_showMunicipalityBordersOverlay = prefs.getBool('map_show_municipality_borders_overlay') ?? false;
|
||||
_showCadastralOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_cadastral_overlay')) ?? false;
|
||||
_showForestRoadsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_forest_roads_overlay')) ?? false;
|
||||
_showHikingTrailsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_hiking_trails_overlay')) ?? false;
|
||||
_showMainRoadsOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_main_roads_overlay')) ?? false;
|
||||
_showHouseNumbersOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_house_numbers_overlay')) ?? false;
|
||||
_showFireHazardZonesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_fire_hazard_zones_overlay')) ??
|
||||
false;
|
||||
_showHistoricalFiresOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_historical_fires_overlay')) ?? false;
|
||||
_showFirebreaksOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_firebreaks_overlay')) ?? false;
|
||||
_showKrasFireZonesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_kras_fire_zones_overlay')) ?? false;
|
||||
_showPlaceNamesOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_place_names_overlay')) ?? false;
|
||||
_showMunicipalityBordersOverlay =
|
||||
prefs.getBool(_scopedKey('map_show_municipality_borders_overlay')) ??
|
||||
false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Save overlay state to SharedPreferences
|
||||
Future<void> _saveOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
|
||||
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
|
||||
await prefs.setBool('map_show_hiking_trails_overlay', _showHikingTrailsOverlay);
|
||||
await prefs.setBool('map_show_main_roads_overlay', _showMainRoadsOverlay);
|
||||
await prefs.setBool('map_show_house_numbers_overlay', _showHouseNumbersOverlay);
|
||||
await prefs.setBool('map_show_fire_hazard_zones_overlay', _showFireHazardZonesOverlay);
|
||||
await prefs.setBool('map_show_historical_fires_overlay', _showHistoricalFiresOverlay);
|
||||
await prefs.setBool('map_show_firebreaks_overlay', _showFirebreaksOverlay);
|
||||
await prefs.setBool('map_show_kras_fire_zones_overlay', _showKrasFireZonesOverlay);
|
||||
await prefs.setBool('map_show_place_names_overlay', _showPlaceNamesOverlay);
|
||||
await prefs.setBool('map_show_municipality_borders_overlay', _showMunicipalityBordersOverlay);
|
||||
Future<void> _loadInitialState() async {
|
||||
await Future.wait([
|
||||
loadOverlayState(),
|
||||
loadTrailSettings(),
|
||||
loadRepeaterVisibilitySettings(),
|
||||
_loadCustomMapState(),
|
||||
]);
|
||||
}
|
||||
|
||||
Future<void> _saveOverlayState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_cadastral_overlay'),
|
||||
_showCadastralOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_forest_roads_overlay'),
|
||||
_showForestRoadsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_hiking_trails_overlay'),
|
||||
_showHikingTrailsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_main_roads_overlay'),
|
||||
_showMainRoadsOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_house_numbers_overlay'),
|
||||
_showHouseNumbersOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_fire_hazard_zones_overlay'),
|
||||
_showFireHazardZonesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_historical_fires_overlay'),
|
||||
_showHistoricalFiresOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_firebreaks_overlay'),
|
||||
_showFirebreaksOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_kras_fire_zones_overlay'),
|
||||
_showKrasFireZonesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_place_names_overlay'),
|
||||
_showPlaceNamesOverlay,
|
||||
);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_municipality_borders_overlay'),
|
||||
_showMunicipalityBordersOverlay,
|
||||
);
|
||||
}
|
||||
|
||||
/// Toggle all contact trails on/off
|
||||
Future<void> toggleAllContactTrails() async {
|
||||
_showAllContactTrails = !_showAllContactTrails;
|
||||
notifyListeners();
|
||||
await _saveTrailSettings();
|
||||
}
|
||||
|
||||
/// Load trail settings from SharedPreferences
|
||||
Future<void> loadTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_showAllContactTrails = prefs.getBool('map_show_all_contact_trails') ?? true; // Default to true (show all)
|
||||
_showAllContactTrails =
|
||||
prefs.getBool(_scopedKey('map_show_all_contact_trails')) ?? true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Save trail settings to SharedPreferences
|
||||
Future<void> _saveTrailSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
|
||||
await prefs.setBool(
|
||||
_scopedKey('map_show_all_contact_trails'),
|
||||
_showAllContactTrails,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setHideRepeatersOnMap(bool hide) async {
|
||||
if (_hideRepeatersOnMap == hide) return;
|
||||
_hideRepeatersOnMap = hide;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_scopedKey('map_hide_repeaters'), _hideRepeatersOnMap);
|
||||
}
|
||||
|
||||
Future<void> loadRepeaterVisibilitySettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_hideRepeatersOnMap =
|
||||
prefs.getBool(_scopedKey('map_hide_repeaters')) ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Set imported trail (from GPX import)
|
||||
void setImportedTrail(LocationTrail trail) {
|
||||
_importedTrail = trail;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Clear imported trail
|
||||
void clearImportedTrail() {
|
||||
_importedTrail = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Replace current trail with imported trail
|
||||
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
||||
// End current trail if active
|
||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||
endTrail();
|
||||
}
|
||||
|
||||
// Set imported trail as current trail
|
||||
_currentTrail = importedTrail;
|
||||
_isTrailVisible = true;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Enter download area selection mode with initial bounds
|
||||
void enterDownloadAreaMode(LatLngBounds initialBounds) {
|
||||
_isSelectingDownloadArea = true;
|
||||
_downloadAreaBounds = initialBounds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Exit download area selection mode
|
||||
void exitDownloadAreaMode() {
|
||||
_isSelectingDownloadArea = false;
|
||||
_downloadAreaBounds = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// Update the download area bounds (while dragging/resizing)
|
||||
void updateDownloadAreaBounds(LatLngBounds bounds) {
|
||||
_downloadAreaBounds = bounds;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _loadCustomMapState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final configJson = prefs.getString(_scopedKey(_customMapConfigKey));
|
||||
if (configJson != null && configJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(configJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
final config = CustomMapConfig.fromJson(decoded);
|
||||
if (config != null && await File(config.filePath).exists()) {
|
||||
_customMapConfig = config;
|
||||
} else {
|
||||
_customMapConfig = null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_customMapConfig = null;
|
||||
}
|
||||
_isUsingCustomMap =
|
||||
(prefs.getBool(_scopedKey(_customMapModeKey)) ?? false) &&
|
||||
_customMapConfig != null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _saveCustomMapState() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (_customMapConfig == null) {
|
||||
await prefs.remove(_scopedKey(_customMapConfigKey));
|
||||
} else {
|
||||
await prefs.setString(
|
||||
_scopedKey(_customMapConfigKey),
|
||||
jsonEncode(_customMapConfig!.toJson()),
|
||||
);
|
||||
}
|
||||
await prefs.setBool(_scopedKey(_customMapModeKey), _isUsingCustomMap);
|
||||
}
|
||||
|
||||
Future<(int, int)> _decodeImageSize(Uint8List bytes) async {
|
||||
final codec = await ui.instantiateImageCodec(bytes);
|
||||
final frame = await codec.getNextFrame();
|
||||
return (frame.image.width, frame.image.height);
|
||||
}
|
||||
|
||||
String _normalizedExtension(String displayName) {
|
||||
final dotIndex = displayName.lastIndexOf('.');
|
||||
if (dotIndex == -1 || dotIndex == displayName.length - 1) {
|
||||
return 'png';
|
||||
}
|
||||
return displayName.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
Future<void> _deleteFileIfExists(String path) async {
|
||||
final file = File(path);
|
||||
if (await file.exists()) {
|
||||
await file.delete();
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, dynamic> exportWorkspaceJson() {
|
||||
return {
|
||||
'currentTrail': _currentTrail?.toJson(),
|
||||
'trailHistory': _trailHistory.map((trail) => trail.toJson()).toList(),
|
||||
'importedTrail': _importedTrail?.toJson(),
|
||||
'isTrailVisible': _isTrailVisible,
|
||||
'showCadastralOverlay': _showCadastralOverlay,
|
||||
'showForestRoadsOverlay': _showForestRoadsOverlay,
|
||||
'showHikingTrailsOverlay': _showHikingTrailsOverlay,
|
||||
'showMainRoadsOverlay': _showMainRoadsOverlay,
|
||||
'showHouseNumbersOverlay': _showHouseNumbersOverlay,
|
||||
'showFireHazardZonesOverlay': _showFireHazardZonesOverlay,
|
||||
'showHistoricalFiresOverlay': _showHistoricalFiresOverlay,
|
||||
'showFirebreaksOverlay': _showFirebreaksOverlay,
|
||||
'showKrasFireZonesOverlay': _showKrasFireZonesOverlay,
|
||||
'showPlaceNamesOverlay': _showPlaceNamesOverlay,
|
||||
'showMunicipalityBordersOverlay': _showMunicipalityBordersOverlay,
|
||||
'showAllContactTrails': _showAllContactTrails,
|
||||
'hideRepeatersOnMap': _hideRepeatersOnMap,
|
||||
};
|
||||
}
|
||||
|
||||
void applyWorkspaceJson(Map<String, dynamic> json) {
|
||||
_currentTrail = json['currentTrail'] is Map<String, dynamic>
|
||||
? LocationTrail.fromJson(json['currentTrail'] as Map<String, dynamic>)
|
||||
: null;
|
||||
_trailHistory
|
||||
..clear()
|
||||
..addAll(
|
||||
(json['trailHistory'] as List<dynamic>? ?? const [])
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(LocationTrail.fromJson),
|
||||
);
|
||||
_importedTrail = json['importedTrail'] is Map<String, dynamic>
|
||||
? LocationTrail.fromJson(json['importedTrail'] as Map<String, dynamic>)
|
||||
: null;
|
||||
_isTrailVisible = json['isTrailVisible'] as bool? ?? true;
|
||||
_showCadastralOverlay = json['showCadastralOverlay'] as bool? ?? false;
|
||||
_showForestRoadsOverlay = json['showForestRoadsOverlay'] as bool? ?? false;
|
||||
_showHikingTrailsOverlay =
|
||||
json['showHikingTrailsOverlay'] as bool? ?? false;
|
||||
_showMainRoadsOverlay = json['showMainRoadsOverlay'] as bool? ?? false;
|
||||
_showHouseNumbersOverlay =
|
||||
json['showHouseNumbersOverlay'] as bool? ?? false;
|
||||
_showFireHazardZonesOverlay =
|
||||
json['showFireHazardZonesOverlay'] as bool? ?? false;
|
||||
_showHistoricalFiresOverlay =
|
||||
json['showHistoricalFiresOverlay'] as bool? ?? false;
|
||||
_showFirebreaksOverlay = json['showFirebreaksOverlay'] as bool? ?? false;
|
||||
_showKrasFireZonesOverlay =
|
||||
json['showKrasFireZonesOverlay'] as bool? ?? false;
|
||||
_showPlaceNamesOverlay = json['showPlaceNamesOverlay'] as bool? ?? false;
|
||||
_showMunicipalityBordersOverlay =
|
||||
json['showMunicipalityBordersOverlay'] as bool? ?? false;
|
||||
_showAllContactTrails = json['showAllContactTrails'] as bool? ?? true;
|
||||
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||
import 'connection_provider.dart';
|
||||
import 'contacts_provider.dart';
|
||||
|
||||
@@ -15,6 +17,19 @@ class SensorsProvider with ChangeNotifier {
|
||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
||||
static const String _metricLabelKey = 'sensor_metric_labels';
|
||||
static const String _metricOrderKey = 'sensor_metric_order';
|
||||
static const String _autoRefreshMinutesKey = 'sensor_auto_refresh_minutes';
|
||||
static const List<int> supportedAutoRefreshIntervals = <int>[
|
||||
0,
|
||||
5,
|
||||
15,
|
||||
30,
|
||||
60,
|
||||
360,
|
||||
720,
|
||||
1440,
|
||||
];
|
||||
static const Set<String> _defaultVisibleFields = <String>{
|
||||
'voltage',
|
||||
'battery',
|
||||
@@ -23,6 +38,14 @@ class SensorsProvider with ChangeNotifier {
|
||||
'pressure',
|
||||
'gps',
|
||||
};
|
||||
static const List<String> _defaultMetricOrder = <String>[
|
||||
'voltage',
|
||||
'battery',
|
||||
'temperature',
|
||||
'humidity',
|
||||
'pressure',
|
||||
'gps',
|
||||
];
|
||||
|
||||
final List<String> _watchedSensorKeys = <String>[];
|
||||
final Map<String, SensorRefreshState> _refreshStates =
|
||||
@@ -32,13 +55,22 @@ class SensorsProvider with ChangeNotifier {
|
||||
<String, Set<String>>{};
|
||||
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
||||
<String, Map<String, int>>{};
|
||||
final Map<String, Map<String, String>> _metricLabelsBySensor =
|
||||
<String, Map<String, String>>{};
|
||||
final Map<String, List<String>> _metricOrderBySensor =
|
||||
<String, List<String>>{};
|
||||
final Map<String, int> _autoRefreshMinutesBySensor = <String, int>{};
|
||||
final Map<String, DateTime> _lastRefreshAttemptAt = <String, DateTime>{};
|
||||
bool _isLoaded = false;
|
||||
bool _isRefreshingAll = false;
|
||||
bool _isRunningAutoRefreshTick = false;
|
||||
|
||||
SensorsProvider() {
|
||||
unawaited(_loadWatchedSensors());
|
||||
}
|
||||
|
||||
String _key(String baseKey) => ProfileStorageScope.scopedKey(baseKey);
|
||||
|
||||
List<String> get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys);
|
||||
bool get isLoaded => _isLoaded;
|
||||
bool get isRefreshingAll => _isRefreshingAll;
|
||||
@@ -49,14 +81,26 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> _loadWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
|
||||
final storedSpansJson = prefs.getString(_fieldSpanKey);
|
||||
final stored =
|
||||
prefs.getStringList(_key(_watchedSensorsKey)) ?? <String>[];
|
||||
final storedMetricsJson = prefs.getString(_key(_visibleSensorMetricsKey));
|
||||
final storedSpansJson = prefs.getString(_key(_fieldSpanKey));
|
||||
final storedLabelsJson = prefs.getString(_key(_metricLabelKey));
|
||||
final storedOrderJson = prefs.getString(_key(_metricOrderKey));
|
||||
final storedAutoRefreshJson = prefs.getString(
|
||||
_key(_autoRefreshMinutesKey),
|
||||
);
|
||||
_watchedSensorKeys
|
||||
..clear()
|
||||
..addAll(stored);
|
||||
_refreshStates.clear();
|
||||
_refreshStateUpdatedAt.clear();
|
||||
_visibleFieldsBySensor.clear();
|
||||
_fieldSpansBySensor.clear();
|
||||
_metricLabelsBySensor.clear();
|
||||
_metricOrderBySensor.clear();
|
||||
_autoRefreshMinutesBySensor.clear();
|
||||
_lastRefreshAttemptAt.clear();
|
||||
if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedMetricsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
@@ -72,12 +116,49 @@ class SensorsProvider with ChangeNotifier {
|
||||
.map((key, value) => MapEntry(key, value as int));
|
||||
}
|
||||
}
|
||||
if (storedLabelsJson != null && storedLabelsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedLabelsJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricLabelsBySensor[entry.key] =
|
||||
(entry.value as Map<String, dynamic>).map(
|
||||
(key, value) => MapEntry(key, value as String),
|
||||
);
|
||||
}
|
||||
}
|
||||
if (storedOrderJson != null && storedOrderJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(storedOrderJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
_metricOrderBySensor[entry.key] = (entry.value as List<dynamic>)
|
||||
.cast<String>()
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
if (storedAutoRefreshJson != null && storedAutoRefreshJson.isNotEmpty) {
|
||||
final decoded =
|
||||
jsonDecode(storedAutoRefreshJson) as Map<String, dynamic>;
|
||||
for (final entry in decoded.entries) {
|
||||
final minutes = _normalizeAutoRefreshMinutes(
|
||||
(entry.value as num).toInt(),
|
||||
);
|
||||
if (minutes > 0) {
|
||||
_autoRefreshMinutesBySensor[entry.key] = minutes;
|
||||
}
|
||||
}
|
||||
}
|
||||
_autoRefreshMinutesBySensor.removeWhere(
|
||||
(key, _) => !_watchedSensorKeys.contains(key),
|
||||
);
|
||||
for (final key in _watchedSensorKeys) {
|
||||
_visibleFieldsBySensor.putIfAbsent(
|
||||
key,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
_fieldSpansBySensor.putIfAbsent(key, () => <String, int>{});
|
||||
_metricLabelsBySensor.putIfAbsent(key, () => <String, String>{});
|
||||
_metricOrderBySensor.putIfAbsent(
|
||||
key,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error loading watched sensors: $e');
|
||||
@@ -87,10 +168,15 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> reloadProfileScopedState() async {
|
||||
_isLoaded = false;
|
||||
await _loadWatchedSensors();
|
||||
}
|
||||
|
||||
Future<void> _persistWatchedSensors() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys);
|
||||
await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving watched sensors: $e');
|
||||
}
|
||||
@@ -103,7 +189,10 @@ class SensorsProvider with ChangeNotifier {
|
||||
for (final entry in _visibleFieldsBySensor.entries) {
|
||||
encoded[entry.key] = entry.value.toList();
|
||||
}
|
||||
await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded));
|
||||
await prefs.setString(
|
||||
_key(_visibleSensorMetricsKey),
|
||||
jsonEncode(encoded),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving visible sensor metrics: $e');
|
||||
}
|
||||
@@ -112,16 +201,71 @@ class SensorsProvider with ChangeNotifier {
|
||||
Future<void> _persistFieldSpans() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_fieldSpanKey, jsonEncode(_fieldSpansBySensor));
|
||||
await prefs.setString(
|
||||
_key(_fieldSpanKey),
|
||||
jsonEncode(_fieldSpansBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor field spans: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricLabels() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_metricLabelKey),
|
||||
jsonEncode(_metricLabelsBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric labels: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistMetricOrder() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_metricOrderKey),
|
||||
jsonEncode(_metricOrderBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor metric order: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _persistAutoRefreshMinutes() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_key(_autoRefreshMinutesKey),
|
||||
jsonEncode(_autoRefreshMinutesBySensor),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('Error saving sensor auto refresh minutes: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> visibleFieldsFor(String publicKeyHex) => Set<String>.unmodifiable(
|
||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
||||
);
|
||||
|
||||
Set<String> effectiveVisibleFieldsFor(
|
||||
String publicKeyHex,
|
||||
Iterable<String> availableFieldKeys,
|
||||
) {
|
||||
final storedVisibleFields =
|
||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields;
|
||||
if (!_shouldAutoIncludeAvailableFields(publicKeyHex, storedVisibleFields)) {
|
||||
return Set<String>.unmodifiable(storedVisibleFields);
|
||||
}
|
||||
|
||||
return Set<String>.unmodifiable(<String>{
|
||||
...storedVisibleFields,
|
||||
...availableFieldKeys,
|
||||
});
|
||||
}
|
||||
|
||||
bool showsField(String publicKeyHex, String fieldKey) =>
|
||||
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
||||
|
||||
@@ -131,6 +275,104 @@ class SensorsProvider with ChangeNotifier {
|
||||
return span == 2 ? 2 : 1;
|
||||
}
|
||||
|
||||
List<String> metricOrderFor(
|
||||
String publicKeyHex,
|
||||
Iterable<String> availableFieldKeys,
|
||||
) {
|
||||
final available = availableFieldKeys.toList();
|
||||
final availableSet = available.toSet();
|
||||
final ordered = <String>[];
|
||||
final seen = <String>{};
|
||||
final stored =
|
||||
_metricOrderBySensor[publicKeyHex] ??
|
||||
List<String>.from(_defaultMetricOrder);
|
||||
|
||||
for (final fieldKey in stored) {
|
||||
if (availableSet.contains(fieldKey) && seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
for (final fieldKey in available) {
|
||||
if (seen.add(fieldKey)) {
|
||||
ordered.add(fieldKey);
|
||||
}
|
||||
}
|
||||
return List<String>.unmodifiable(ordered);
|
||||
}
|
||||
|
||||
Map<String, String> labelOverridesFor(String publicKeyHex) =>
|
||||
Map<String, String>.unmodifiable(
|
||||
_metricLabelsBySensor[publicKeyHex] ?? const <String, String>{},
|
||||
);
|
||||
|
||||
String? labelOverrideFor(String publicKeyHex, String fieldKey) =>
|
||||
_metricLabelsBySensor[publicKeyHex]?[fieldKey];
|
||||
|
||||
int autoRefreshMinutesFor(String publicKeyHex) =>
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] ?? 0;
|
||||
|
||||
Future<void> setAutoRefreshMinutes(String publicKeyHex, int minutes) async {
|
||||
final normalizedMinutes = _normalizeAutoRefreshMinutes(minutes);
|
||||
final currentMinutes = autoRefreshMinutesFor(publicKeyHex);
|
||||
if (currentMinutes == normalizedMinutes) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (normalizedMinutes == 0) {
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
} else {
|
||||
_autoRefreshMinutesBySensor[publicKeyHex] = normalizedMinutes;
|
||||
}
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
static int _normalizeAutoRefreshMinutes(int minutes) {
|
||||
if (minutes <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (supportedAutoRefreshIntervals.contains(minutes)) {
|
||||
return minutes;
|
||||
}
|
||||
|
||||
final positiveIntervals = supportedAutoRefreshIntervals.where(
|
||||
(value) => value > 0,
|
||||
);
|
||||
var bestMatch = positiveIntervals.first;
|
||||
var bestDistance = (minutes - bestMatch).abs();
|
||||
|
||||
for (final interval in positiveIntervals.skip(1)) {
|
||||
final distance = (minutes - interval).abs();
|
||||
if (distance < bestDistance) {
|
||||
bestMatch = interval;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
List<String> dueAutoRefreshSensorKeys({DateTime? now}) {
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = <String>[];
|
||||
|
||||
for (final key in _watchedSensorKeys) {
|
||||
final minutes = autoRefreshMinutesFor(key);
|
||||
if (minutes <= 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final lastRefreshAt = _lastRefreshAttemptAt[key];
|
||||
if (lastRefreshAt == null ||
|
||||
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes)) {
|
||||
dueKeys.add(key);
|
||||
}
|
||||
}
|
||||
|
||||
return List<String>.unmodifiable(dueKeys);
|
||||
}
|
||||
|
||||
Future<void> toggleMetric(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
@@ -140,8 +382,17 @@ class SensorsProvider with ChangeNotifier {
|
||||
publicKeyHex,
|
||||
() => Set<String>.from(_defaultVisibleFields),
|
||||
);
|
||||
final metricOrder = _metricOrderBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => List<String>.from(_defaultMetricOrder),
|
||||
);
|
||||
var shouldPersistOrder = false;
|
||||
if (visible) {
|
||||
visibleFields.add(fieldKey);
|
||||
if (!metricOrder.contains(fieldKey)) {
|
||||
metricOrder.add(fieldKey);
|
||||
shouldPersistOrder = true;
|
||||
}
|
||||
} else {
|
||||
if (visibleFields.length == 1 && visibleFields.contains(fieldKey)) {
|
||||
return;
|
||||
@@ -149,6 +400,9 @@ class SensorsProvider with ChangeNotifier {
|
||||
visibleFields.remove(fieldKey);
|
||||
}
|
||||
await _persistVisibleMetrics();
|
||||
if (shouldPersistOrder) {
|
||||
await _persistMetricOrder();
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@@ -166,11 +420,77 @@ class SensorsProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setMetricLabel(
|
||||
String publicKeyHex,
|
||||
String fieldKey,
|
||||
String? label,
|
||||
) async {
|
||||
final sensorLabels = _metricLabelsBySensor.putIfAbsent(
|
||||
publicKeyHex,
|
||||
() => <String, String>{},
|
||||
);
|
||||
final trimmed = label?.trim();
|
||||
if (trimmed == null || trimmed.isEmpty) {
|
||||
sensorLabels.remove(fieldKey);
|
||||
} else {
|
||||
sensorLabels[fieldKey] = trimmed;
|
||||
}
|
||||
if (sensorLabels.isEmpty) {
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
}
|
||||
await _persistMetricLabels();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> moveMetric(
|
||||
String publicKeyHex, {
|
||||
required List<String> availableFieldKeys,
|
||||
required int oldIndex,
|
||||
required int newIndex,
|
||||
}) async {
|
||||
if (oldIndex < 0 ||
|
||||
newIndex < 0 ||
|
||||
oldIndex >= availableFieldKeys.length ||
|
||||
newIndex >= availableFieldKeys.length ||
|
||||
oldIndex == newIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
final reordered = List<String>.from(
|
||||
metricOrderFor(publicKeyHex, availableFieldKeys),
|
||||
);
|
||||
final fieldKey = reordered.removeAt(oldIndex);
|
||||
reordered.insert(newIndex, fieldKey);
|
||||
|
||||
final storedTail =
|
||||
(_metricOrderBySensor[publicKeyHex] ?? _defaultMetricOrder).where(
|
||||
(key) => !reordered.contains(key),
|
||||
);
|
||||
_metricOrderBySensor[publicKeyHex] = <String>[...reordered, ...storedTail];
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool isWatched(String publicKeyHex) =>
|
||||
_watchedSensorKeys.contains(publicKeyHex);
|
||||
|
||||
Contact? selfContact(
|
||||
ContactsProvider contactsProvider,
|
||||
ConnectionProvider connectionProvider,
|
||||
) {
|
||||
final selfKey = connectionProvider.deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final existing = contactsProvider.findContactByKey(
|
||||
Uint8List.fromList(selfKey),
|
||||
);
|
||||
return existing ?? _buildSelfCandidate(connectionProvider);
|
||||
}
|
||||
|
||||
Future<void> addSensor(Contact contact) async {
|
||||
if (!contact.isChat && !contact.isRepeater) {
|
||||
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
||||
return;
|
||||
}
|
||||
if (_watchedSensorKeys.contains(contact.publicKeyHex)) {
|
||||
@@ -178,38 +498,131 @@ class SensorsProvider with ChangeNotifier {
|
||||
}
|
||||
|
||||
_watchedSensorKeys.add(contact.publicKeyHex);
|
||||
final initialVisibleFields = _initialVisibleFieldsForContact(contact);
|
||||
await _persistWatchedSensors();
|
||||
_visibleFieldsBySensor[contact.publicKeyHex] = Set<String>.from(
|
||||
_defaultVisibleFields,
|
||||
);
|
||||
_visibleFieldsBySensor[contact.publicKeyHex] = initialVisibleFields;
|
||||
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
||||
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
||||
_metricOrderBySensor[contact.publicKeyHex] = metricOrderFor(
|
||||
contact.publicKeyHex,
|
||||
initialVisibleFields,
|
||||
);
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
bool _shouldAutoIncludeAvailableFields(
|
||||
String publicKeyHex,
|
||||
Set<String> storedVisibleFields,
|
||||
) {
|
||||
if (!setEquals(storedVisibleFields, _defaultVisibleFields)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedOrder = _metricOrderBySensor[publicKeyHex];
|
||||
if (storedOrder != null && !listEquals(storedOrder, _defaultMetricOrder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedLabels = _metricLabelsBySensor[publicKeyHex];
|
||||
if (storedLabels != null && storedLabels.isNotEmpty) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final storedSpans = _fieldSpansBySensor[publicKeyHex];
|
||||
if (storedSpans != null &&
|
||||
!(storedSpans.length == 1 && storedSpans['gps'] == 2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Set<String> _initialVisibleFieldsForContact(Contact contact) {
|
||||
return <String>{..._defaultVisibleFields, ...sensorMetricKeysFor(contact)};
|
||||
}
|
||||
|
||||
Future<void> removeSensor(String publicKeyHex) async {
|
||||
_watchedSensorKeys.remove(publicKeyHex);
|
||||
_refreshStates.remove(publicKeyHex);
|
||||
_refreshStateUpdatedAt.remove(publicKeyHex);
|
||||
_visibleFieldsBySensor.remove(publicKeyHex);
|
||||
_fieldSpansBySensor.remove(publicKeyHex);
|
||||
_metricLabelsBySensor.remove(publicKeyHex);
|
||||
_metricOrderBySensor.remove(publicKeyHex);
|
||||
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||
_lastRefreshAttemptAt.remove(publicKeyHex);
|
||||
await _persistWatchedSensors();
|
||||
await _persistVisibleMetrics();
|
||||
await _persistFieldSpans();
|
||||
await _persistMetricLabels();
|
||||
await _persistMetricOrder();
|
||||
await _persistAutoRefreshMinutes();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
|
||||
List<Contact> availableCandidates(
|
||||
ContactsProvider contactsProvider, {
|
||||
ConnectionProvider? connectionProvider,
|
||||
}) {
|
||||
final candidates = <Contact>[
|
||||
...contactsProvider.chatContacts,
|
||||
...contactsProvider.repeaters,
|
||||
...contactsProvider.sensorContacts,
|
||||
];
|
||||
|
||||
// Add "myself" (own device) as a candidate if it has a public key
|
||||
if (connectionProvider != null) {
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self != null &&
|
||||
!candidates.any((c) => c.publicKeyHex == self.publicKeyHex)) {
|
||||
candidates.insert(0, self);
|
||||
}
|
||||
}
|
||||
|
||||
candidates.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
||||
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||
return candidates;
|
||||
}
|
||||
|
||||
List<String> displaySensorKeys({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
if (_watchedSensorKeys.isNotEmpty) {
|
||||
return List<String>.unmodifiable(_watchedSensorKeys);
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self == null) {
|
||||
return const <String>[];
|
||||
}
|
||||
|
||||
return <String>[self.publicKeyHex];
|
||||
}
|
||||
|
||||
Contact? contactForDisplay(
|
||||
String publicKeyHex, {
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
}) {
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
final self = selfContact(contactsProvider, connectionProvider);
|
||||
if (self?.publicKeyHex == publicKeyHex) {
|
||||
return self;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> refreshAll({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
@@ -239,15 +652,20 @@ class SensorsProvider with ChangeNotifier {
|
||||
required String publicKeyHex,
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? requestedAt,
|
||||
}) async {
|
||||
Contact? contact;
|
||||
for (final entry in contactsProvider.contacts) {
|
||||
if (entry.publicKeyHex == publicKeyHex) {
|
||||
contact = entry;
|
||||
break;
|
||||
}
|
||||
if (stateFor(publicKeyHex) == SensorRefreshState.refreshing) {
|
||||
return;
|
||||
}
|
||||
|
||||
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||
|
||||
final contact = contactForDisplay(
|
||||
publicKeyHex,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
);
|
||||
|
||||
if (contact == null) {
|
||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||
return;
|
||||
@@ -266,6 +684,38 @@ class SensorsProvider with ChangeNotifier {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> refreshDueSensors({
|
||||
required ContactsProvider contactsProvider,
|
||||
required ConnectionProvider connectionProvider,
|
||||
DateTime? now,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected ||
|
||||
_isRefreshingAll ||
|
||||
_isRunningAutoRefreshTick) {
|
||||
return;
|
||||
}
|
||||
|
||||
final refreshTime = now ?? DateTime.now();
|
||||
final dueKeys = dueAutoRefreshSensorKeys(now: refreshTime);
|
||||
if (dueKeys.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isRunningAutoRefreshTick = true;
|
||||
try {
|
||||
for (final key in dueKeys) {
|
||||
await refreshSensor(
|
||||
publicKeyHex: key,
|
||||
contactsProvider: contactsProvider,
|
||||
connectionProvider: connectionProvider,
|
||||
requestedAt: refreshTime,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
_isRunningAutoRefreshTick = false;
|
||||
}
|
||||
}
|
||||
|
||||
void clearExpiredRefreshStates({DateTime? now}) {
|
||||
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
|
||||
final keysToClear = <String>[];
|
||||
@@ -297,4 +747,25 @@ class SensorsProvider with ChangeNotifier {
|
||||
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Contact? _buildSelfCandidate(ConnectionProvider connectionProvider) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final selfKey = deviceInfo.publicKey;
|
||||
if (selfKey == null || selfKey.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(selfKey),
|
||||
type: ContactType.chat,
|
||||
flags: 0,
|
||||
outPathLen: 0,
|
||||
outPath: Uint8List(64),
|
||||
advName: deviceInfo.selfName ?? 'My Device',
|
||||
lastAdvert: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
advLat: deviceInfo.advLat ?? 0,
|
||||
advLon: deviceInfo.advLon ?? 0,
|
||||
lastMod: DateTime.now().millisecondsSinceEpoch ~/ 1000,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
@@ -299,11 +300,15 @@ class VoiceProvider with ChangeNotifier {
|
||||
);
|
||||
|
||||
try {
|
||||
final pcm = await _codec.decodePackets(session.packets, session.mode);
|
||||
final decodedPcm = await _codec.decodePackets(
|
||||
session.packets,
|
||||
session.mode,
|
||||
);
|
||||
final pcm = _preparePlaybackPcm(decodedPcm, session.mode);
|
||||
debugPrint('🎙️ [VoiceProvider] decoded ${pcm.length} PCM samples');
|
||||
_playingSessionId = sessionId;
|
||||
notifyListeners();
|
||||
await _player.play(pcm);
|
||||
await _player.play(pcm, sampleRateHz: session.mode.sampleRateHz);
|
||||
} catch (e, st) {
|
||||
debugPrint('❌ [VoiceProvider] Playback error: $e\n$st');
|
||||
if (_playingSessionId == sessionId) {
|
||||
@@ -319,6 +324,10 @@ class VoiceProvider with ChangeNotifier {
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Int16List _preparePlaybackPcm(Int16List pcm, VoicePacketMode mode) {
|
||||
return pcm;
|
||||
}
|
||||
|
||||
Future<void> clearStoredVoiceData() async {
|
||||
_sessions.clear();
|
||||
_outgoingSessions.clear();
|
||||
|
||||
387
lib/screens/add_contact_screen.dart
Normal file
387
lib/screens/add_contact_screen.dart
Normal file
@@ -0,0 +1,387 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class AddContactScreen extends StatefulWidget {
|
||||
const AddContactScreen({super.key});
|
||||
|
||||
@override
|
||||
State<AddContactScreen> createState() => _AddContactScreenState();
|
||||
}
|
||||
|
||||
class _AddContactScreenState extends State<AddContactScreen> {
|
||||
final TextEditingController _advertController = TextEditingController();
|
||||
bool _isImporting = false;
|
||||
bool _importSucceeded = false;
|
||||
String? _validationError;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadClipboardIfPresent();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_advertController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadClipboardIfPresent() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = clipboardData?.text?.trim();
|
||||
if (!mounted || text == null || text.isEmpty) {
|
||||
return;
|
||||
}
|
||||
if (_normalizeAdvertText(text) == null) {
|
||||
return;
|
||||
}
|
||||
_advertController.text = text;
|
||||
}
|
||||
|
||||
String? _normalizeAdvertText(String value) {
|
||||
final trimmed = value.trim();
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var normalized = trimmed;
|
||||
if (normalized.startsWith('meshcore://')) {
|
||||
normalized = normalized.substring('meshcore://'.length);
|
||||
}
|
||||
|
||||
normalized = normalized.replaceAll(RegExp(r'\s+'), '');
|
||||
if (normalized.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final isHex = RegExp(r'^[0-9a-fA-F]+$').hasMatch(normalized);
|
||||
if (!isHex || normalized.length.isOdd) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return normalized.toLowerCase();
|
||||
}
|
||||
|
||||
Uint8List _hexToBytes(String hex) {
|
||||
final bytes = <int>[];
|
||||
for (var i = 0; i < hex.length; i += 2) {
|
||||
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
||||
}
|
||||
return Uint8List.fromList(bytes);
|
||||
}
|
||||
|
||||
Future<void> _pasteFromClipboard() async {
|
||||
final clipboardData = await Clipboard.getData(Clipboard.kTextPlain);
|
||||
final text = clipboardData?.text;
|
||||
if (text == null || text.trim().isEmpty) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.clipboardIsEmpty)));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_advertController.text = text.trim();
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _importContact() async {
|
||||
final normalized = _normalizeAdvertText(_advertController.text);
|
||||
if (normalized == null) {
|
||||
setState(() {
|
||||
_validationError =
|
||||
'Enter a valid meshcore:// advert or raw hexadecimal contact advert.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
final advertBytes = _hexToBytes(normalized);
|
||||
if (advertBytes.length < 98) {
|
||||
setState(() {
|
||||
_validationError =
|
||||
'Advert is too short. Expected exported contact data.';
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isImporting = true;
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
await connectionProvider.importContactAdvert(advertBytes);
|
||||
final importError = connectionProvider.error;
|
||||
|
||||
if (importError == null) {
|
||||
await connectionProvider.getContacts();
|
||||
}
|
||||
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isImporting = false;
|
||||
});
|
||||
|
||||
if (importError != null) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(importError)));
|
||||
return;
|
||||
}
|
||||
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.contactImported)));
|
||||
setState(() {
|
||||
_importSucceeded = true;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final normalized = _normalizeAdvertText(_advertController.text);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.addContact)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest.withValues(alpha: 0.5),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(
|
||||
Icons.person_add_alt_1_outlined,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Import a shared contact advert',
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'Paste a `meshcore://...` link or raw hexadecimal advert. The app will validate it and import the contact into the connected device.',
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_ImportHintChip(
|
||||
icon: Icons.link_outlined,
|
||||
label: AppLocalizations.of(context)!.acceptsShareLinks,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.code_outlined,
|
||||
label: AppLocalizations.of(context)!.supportsRawHex,
|
||||
),
|
||||
_ImportHintChip(
|
||||
icon: Icons.content_paste_go_outlined,
|
||||
label: AppLocalizations.of(context)!.clipboardfriendly,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: colorScheme.outlineVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Advert payload',
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'You can paste the full share link or only the hex payload.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
TextField(
|
||||
controller: _advertController,
|
||||
minLines: 5,
|
||||
maxLines: 9,
|
||||
onChanged: (_) {
|
||||
if (_validationError != null || _importSucceeded) {
|
||||
setState(() {
|
||||
_importSucceeded = false;
|
||||
_validationError = null;
|
||||
});
|
||||
}
|
||||
},
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Contact advert',
|
||||
hintText: 'Paste a share link or hex advert',
|
||||
alignLabelWithHint: true,
|
||||
border: const OutlineInputBorder(),
|
||||
errorText: _validationError,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
normalized == null
|
||||
? 'No valid advert detected yet'
|
||||
: 'Advert size: ${normalized.length ~/ 2} bytes',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _isImporting ? null : _pasteFromClipboard,
|
||||
icon: Icon(Icons.content_paste_go_outlined),
|
||||
label: Text(AppLocalizations.of(context)!.paste),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
FilledButton.icon(
|
||||
onPressed: (_isImporting || _importSucceeded)
|
||||
? null
|
||||
: _importContact,
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
),
|
||||
icon: _isImporting
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: _importSucceeded
|
||||
? const Icon(Icons.check_circle_outline)
|
||||
: const Icon(Icons.person_add_alt_1_outlined),
|
||||
label: Text(
|
||||
_isImporting
|
||||
? 'Importing...'
|
||||
: _importSucceeded
|
||||
? 'Imported'
|
||||
: 'Add Contact',
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
if (_importSucceeded)
|
||||
Container(
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.primary.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: colorScheme.primary.withValues(alpha: 0.18),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
size: 18,
|
||||
color: colorScheme.primary,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Contact imported. Paste another advert or edit the current one to import again.',
|
||||
style: theme.textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ImportHintChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
|
||||
const _ImportHintChip({required this.icon, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
color: theme.colorScheme.outlineVariant.withValues(alpha: 0.7),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 16, color: theme.colorScheme.onSurfaceVariant),
|
||||
const SizedBox(width: 6),
|
||||
Text(label, style: theme.textTheme.labelMedium),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
732
lib/screens/discovery_screen.dart
Normal file
732
lib/screens/discovery_screen.dart
Normal file
@@ -0,0 +1,732 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../widgets/compact_signal_indicator.dart' show SignalMetric;
|
||||
|
||||
enum _DiscoveryMenuAction { repeaters, sensors }
|
||||
|
||||
class DiscoveryScreen extends StatefulWidget {
|
||||
const DiscoveryScreen({super.key});
|
||||
|
||||
@override
|
||||
State<DiscoveryScreen> createState() => _DiscoveryScreenState();
|
||||
}
|
||||
|
||||
class _DiscoveryScreenState extends State<DiscoveryScreen> {
|
||||
static const int _repeaterAdvertType = 2;
|
||||
static const int _sensorAdvertType = 4;
|
||||
final Set<String> _resolvingAdvertKeys = <String>{};
|
||||
final Set<int> _runningDiscoveryTypes = <int>{};
|
||||
bool _isResolvingAll = false;
|
||||
late final Future<List<MeshMapNode>> _cachedNodesFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_cachedNodesFuture = MeshMapNodesService.loadCachedNodes(
|
||||
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleMenuAction(_DiscoveryMenuAction action) async {
|
||||
switch (action) {
|
||||
case _DiscoveryMenuAction.repeaters:
|
||||
await _discoverNodeType(_repeaterAdvertType);
|
||||
break;
|
||||
case _DiscoveryMenuAction.sensors:
|
||||
await _discoverNodeType(_sensorAdvertType);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clearAllDiscoveries() async {
|
||||
final pendingCount = context.read<ContactsProvider>().pendingAdverts.length;
|
||||
if (pendingCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.clearDiscoveries),
|
||||
content: Text(
|
||||
'Remove all $pendingCount pending discover${pendingCount == 1 ? 'y' : 'ies'} from this device?',
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: Text(AppLocalizations.of(context)!.clearAllLabel),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (confirmed != true || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
await context.read<ContactsProvider>().clearPendingAdverts();
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.clearedPendingDiscoveries)),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _discoverNodeType(int advertType) async {
|
||||
if (_runningDiscoveryTypes.contains(advertType)) return;
|
||||
|
||||
setState(() {
|
||||
_runningDiscoveryTypes.add(advertType);
|
||||
});
|
||||
|
||||
try {
|
||||
await context.read<ConnectionProvider>().discoverNodeType(
|
||||
advertType: advertType,
|
||||
);
|
||||
if (!mounted) return;
|
||||
final label = switch (advertType) {
|
||||
_repeaterAdvertType => 'Repeater discovery sent',
|
||||
_sensorAdvertType => 'Sensor discovery sent',
|
||||
_ => 'Discovery sent',
|
||||
};
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(label)));
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_runningDiscoveryTypes.remove(advertType);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolveAdvert(PendingAdvert advert) async {
|
||||
final keyHex = advert.publicKeyHex;
|
||||
if (_resolvingAdvertKeys.contains(keyHex)) return;
|
||||
|
||||
setState(() {
|
||||
_resolvingAdvertKeys.add(keyHex);
|
||||
});
|
||||
|
||||
try {
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
|
||||
final canAddDirectly = _canAddPendingAdvertDirectly(advert);
|
||||
if (canAddDirectly) {
|
||||
final added = await _addPendingAdvertToRadio(
|
||||
advert,
|
||||
connectionProvider: connectionProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
);
|
||||
if (!added) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
await connectionProvider.getContact(advert.publicKey);
|
||||
if (connectionProvider.error == 'Not found') {
|
||||
connectionProvider.clearError();
|
||||
final added = await _addPendingAdvertToRadio(
|
||||
advert,
|
||||
connectionProvider: connectionProvider,
|
||||
contactsProvider: contactsProvider,
|
||||
);
|
||||
if (!added) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
if ((advert.typeValue ?? 0) == _sensorAdvertType) {
|
||||
await connectionProvider.requestTelemetry(advert.publicKey);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_resolvingAdvertKeys.remove(keyHex);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _resolveAll(List<PendingAdvert> adverts) async {
|
||||
if (_isResolvingAll || adverts.isEmpty) return;
|
||||
|
||||
setState(() {
|
||||
_isResolvingAll = true;
|
||||
});
|
||||
|
||||
try {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
for (final advert in adverts) {
|
||||
if (!mounted) break;
|
||||
// Skip already-resolved contacts
|
||||
if (contactsProvider.findContactByKey(advert.publicKey) != null) {
|
||||
continue;
|
||||
}
|
||||
await _resolveAdvert(advert);
|
||||
}
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isResolvingAll = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
String _formatRelativeTime(BuildContext context, DateTime when) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final diff = DateTime.now().difference(when);
|
||||
if (diff.inMinutes < 1) return l10n.justNow;
|
||||
if (diff.inMinutes < 60) return l10n.minutesAgo(diff.inMinutes);
|
||||
if (diff.inHours < 24) return l10n.hoursAgo(diff.inHours);
|
||||
return l10n.daysAgo(diff.inDays);
|
||||
}
|
||||
|
||||
Contact _contactFromPendingAdvert(PendingAdvert advert) {
|
||||
final lastAdvert =
|
||||
advert.lastAdvert ?? (advert.receivedAt.millisecondsSinceEpoch ~/ 1000);
|
||||
final advName = advert.advName?.trim().isNotEmpty == true
|
||||
? advert.advName!.trim()
|
||||
: advert.shortDisplayKey;
|
||||
|
||||
return Contact(
|
||||
publicKey: Uint8List.fromList(advert.publicKey),
|
||||
type: ContactType.fromValue(advert.typeValue ?? 0),
|
||||
flags: advert.flags ?? 0,
|
||||
outPathLen: advert.signedEncodedPathLen ?? -1,
|
||||
outPath: advert.paddedPathBytes == null
|
||||
? Uint8List(64)
|
||||
: Uint8List.fromList(advert.paddedPathBytes!),
|
||||
advName: advName,
|
||||
lastAdvert: lastAdvert,
|
||||
advLat: advert.advLat ?? 0,
|
||||
advLon: advert.advLon ?? 0,
|
||||
lastMod: lastAdvert,
|
||||
);
|
||||
}
|
||||
|
||||
bool _canAddPendingAdvertDirectly(PendingAdvert advert) {
|
||||
return advert.publicKey.length == 32 &&
|
||||
advert.typeValue != null &&
|
||||
advert.typeValue != 0;
|
||||
}
|
||||
|
||||
Future<bool> _addPendingAdvertToRadio(
|
||||
PendingAdvert advert, {
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ContactsProvider contactsProvider,
|
||||
}) async {
|
||||
final contact = _contactFromPendingAdvert(advert);
|
||||
await connectionProvider.addOrUpdateContact(contact);
|
||||
|
||||
final addError = connectionProvider.error;
|
||||
if (addError != null) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to add contact: $addError')),
|
||||
);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
contactsProvider.addOrUpdateContact(
|
||||
contact,
|
||||
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
String _displayNameForAdvert(
|
||||
PendingAdvert advert,
|
||||
ContactsProvider contactsProvider,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
final advertisedName = advert.advName?.trim();
|
||||
if (advertisedName != null && advertisedName.isNotEmpty) {
|
||||
return advertisedName;
|
||||
}
|
||||
|
||||
Contact? existingMatch;
|
||||
for (final contact in contactsProvider.contacts) {
|
||||
if (contact.publicKeyHex == advert.publicKeyHex) {
|
||||
existingMatch = contact;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (existingMatch != null && existingMatch.displayName.trim().isNotEmpty) {
|
||||
return existingMatch.displayName;
|
||||
}
|
||||
|
||||
MeshMapNode? cachedMatch;
|
||||
for (final node in cachedNodes) {
|
||||
if (node.publicKey == advert.publicKeyHex.toLowerCase()) {
|
||||
cachedMatch = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (cachedMatch != null && cachedMatch.name.trim().isNotEmpty) {
|
||||
return cachedMatch.name;
|
||||
}
|
||||
|
||||
return advert.shortDisplayKey;
|
||||
}
|
||||
|
||||
IconData _iconForAdvert(PendingAdvert advert) {
|
||||
return switch (advert.typeValue) {
|
||||
_repeaterAdvertType => Icons.router_outlined,
|
||||
_sensorAdvertType => Icons.sensors_outlined,
|
||||
_ => Icons.campaign_outlined,
|
||||
};
|
||||
}
|
||||
|
||||
String? _typeLabelForAdvert(PendingAdvert advert) {
|
||||
return switch (advert.typeValue) {
|
||||
_repeaterAdvertType => 'Repeater',
|
||||
_sensorAdvertType => 'Sensor',
|
||||
3 => 'Room',
|
||||
1 => 'Chat',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
String? _resolvedTypeLabelForAdvert(
|
||||
PendingAdvert advert,
|
||||
List<MeshMapNode> cachedNodes,
|
||||
) {
|
||||
final directType = _typeLabelForAdvert(advert);
|
||||
if (directType != null) {
|
||||
return directType;
|
||||
}
|
||||
|
||||
for (final node in cachedNodes) {
|
||||
if (node.publicKey == advert.publicKeyHex.toLowerCase()) {
|
||||
return switch (node.type) {
|
||||
1 => 'Repeater',
|
||||
4 => 'Sensor',
|
||||
3 => 'Room',
|
||||
2 => 'Chat',
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
Widget _buildAdvertTitle(
|
||||
BuildContext context, {
|
||||
required String displayName,
|
||||
String? subtitle,
|
||||
}) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
displayName,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (subtitle != null && subtitle.isNotEmpty) ...[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
subtitle,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(l10n.discovery),
|
||||
actions: [
|
||||
Consumer<ConnectionProvider>(
|
||||
builder: (context, connectionProvider, child) {
|
||||
final isConnected = connectionProvider.deviceInfo.isConnected;
|
||||
final repeatersBusy = _runningDiscoveryTypes.contains(
|
||||
_repeaterAdvertType,
|
||||
);
|
||||
final sensorsBusy = _runningDiscoveryTypes.contains(
|
||||
_sensorAdvertType,
|
||||
);
|
||||
|
||||
return PopupMenuButton<_DiscoveryMenuAction>(
|
||||
tooltip: 'Discovery tools',
|
||||
onSelected: _handleMenuAction,
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem<_DiscoveryMenuAction>(
|
||||
value: _DiscoveryMenuAction.repeaters,
|
||||
enabled: isConnected && !repeatersBusy,
|
||||
child: Row(
|
||||
children: [
|
||||
repeatersBusy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.router_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(l10n.discoverRepeaters),
|
||||
],
|
||||
),
|
||||
),
|
||||
PopupMenuItem<_DiscoveryMenuAction>(
|
||||
value: _DiscoveryMenuAction.sensors,
|
||||
enabled: isConnected && !sensorsBusy,
|
||||
child: Row(
|
||||
children: [
|
||||
sensorsBusy
|
||||
? const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.sensors_outlined),
|
||||
SizedBox(width: 12),
|
||||
Text(l10n.discoverSensors),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
body: FutureBuilder<List<MeshMapNode>>(
|
||||
future: _cachedNodesFuture,
|
||||
builder: (context, nodesSnapshot) => Consumer2<ContactsProvider, ConnectionProvider>(
|
||||
builder: (context, contactsProvider, connectionProvider, child) {
|
||||
final pendingAdverts = contactsProvider.pendingAdverts;
|
||||
final isConnected = connectionProvider.deviceInfo.isConnected;
|
||||
final cachedNodes = nodesSnapshot.data ?? const <MeshMapNode>[];
|
||||
|
||||
// Track which pending adverts are already in the contacts list
|
||||
final resolvedKeySet = <String>{};
|
||||
for (final advert in pendingAdverts) {
|
||||
if (contactsProvider.findContactByKey(advert.publicKey) != null) {
|
||||
resolvedKeySet.add(advert.publicKeyHex);
|
||||
}
|
||||
}
|
||||
|
||||
final totalCount = pendingAdverts.length;
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.person_search),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'Discovered nodes ($totalCount)',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Resolve entries manually so they do not auto-populate contacts.',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed:
|
||||
isConnected &&
|
||||
pendingAdverts.isNotEmpty &&
|
||||
!_isResolvingAll
|
||||
? () => _resolveAll(pendingAdverts)
|
||||
: null,
|
||||
icon: _isResolvingAll
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.download_for_offline_outlined,
|
||||
),
|
||||
label: Text(l10n.resolveAll),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
Expanded(
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: pendingAdverts.isNotEmpty
|
||||
? _clearAllDiscoveries
|
||||
: null,
|
||||
icon: Icon(Icons.clear_all_rounded),
|
||||
label: Text(l10n.clearAllLabel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (totalCount == 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 48),
|
||||
child: Column(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.person_search_outlined,
|
||||
size: 64,
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'No discovered nodes',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Use the menu to discover repeaters and sensors on the mesh.',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
...pendingAdverts.map((advert) =>
|
||||
_buildPendingAdvertCard(
|
||||
context,
|
||||
advert: advert,
|
||||
contactsProvider: contactsProvider,
|
||||
isConnected: isConnected,
|
||||
isResolved: resolvedKeySet.contains(advert.publicKeyHex),
|
||||
cachedNodes: cachedNodes,
|
||||
l10n: l10n,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPendingAdvertCard(
|
||||
BuildContext context, {
|
||||
required PendingAdvert advert,
|
||||
required ContactsProvider contactsProvider,
|
||||
required bool isConnected,
|
||||
required bool isResolved,
|
||||
required List<MeshMapNode> cachedNodes,
|
||||
required AppLocalizations l10n,
|
||||
}) {
|
||||
final isResolving = _resolvingAdvertKeys.contains(advert.publicKeyHex);
|
||||
final displayName = _displayNameForAdvert(advert, contactsProvider, cachedNodes);
|
||||
final typeLabel = _resolvedTypeLabelForAdvert(advert, cachedNodes);
|
||||
final downMetric = SignalMetric.fromValues(
|
||||
rssiDbm: advert.rxRssiDbm,
|
||||
snrDb: advert.rxSnr,
|
||||
);
|
||||
final upMetric = SignalMetric.fromValues(
|
||||
rssiDbm: advert.repeaterLastRssi,
|
||||
snrDb: advert.repeaterLastSnr,
|
||||
);
|
||||
final detailLines = <String>[
|
||||
'${l10n.publicKey}: ${advert.shortDisplayKey}',
|
||||
];
|
||||
final summaryParts = <String>[];
|
||||
final battery = advert.repeaterBatteryPercent;
|
||||
if (battery != null) {
|
||||
summaryParts.add('Battery ${battery.round()}%');
|
||||
}
|
||||
if (advert.repeaterQueueLen != null) {
|
||||
summaryParts.add('Queue ${advert.repeaterQueueLen}');
|
||||
}
|
||||
if (summaryParts.isNotEmpty) {
|
||||
detailLines.add(summaryParts.join(' • '));
|
||||
}
|
||||
detailLines.add(
|
||||
'${l10n.lastSeen}: ${_formatRelativeTime(context, advert.receivedAt)}',
|
||||
);
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(14),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
CircleAvatar(child: Icon(_iconForAdvert(advert))),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildAdvertTitle(
|
||||
context,
|
||||
displayName: displayName,
|
||||
subtitle: typeLabel,
|
||||
),
|
||||
),
|
||||
if (downMetric != null || upMetric != null) ...[
|
||||
const SizedBox(width: 12),
|
||||
_buildSignalSummary(
|
||||
context,
|
||||
downMetric: downMetric,
|
||||
upMetric: upMetric,
|
||||
),
|
||||
],
|
||||
const SizedBox(width: 8),
|
||||
if (isResolved)
|
||||
Icon(
|
||||
Icons.check_circle,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 24,
|
||||
)
|
||||
else if (isResolving)
|
||||
const SizedBox(
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
else
|
||||
IconButton(
|
||||
visualDensity: VisualDensity.compact,
|
||||
icon: const Icon(Icons.person_add_alt_1),
|
||||
tooltip: 'Resolve contact',
|
||||
onPressed: isConnected
|
||||
? () => _resolveAdvert(advert)
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
detailLines.join('\n'),
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSignalSummary(
|
||||
BuildContext context, {
|
||||
SignalMetric? downMetric,
|
||||
SignalMetric? upMetric,
|
||||
}) {
|
||||
if (downMetric == null && upMetric == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
if (downMetric != null)
|
||||
_buildDirectionalSignal(
|
||||
context,
|
||||
icon: Icons.south_west_rounded,
|
||||
metric: downMetric,
|
||||
),
|
||||
if (downMetric != null && upMetric != null) const SizedBox(height: 6),
|
||||
if (upMetric != null)
|
||||
_buildDirectionalSignal(
|
||||
context,
|
||||
icon: Icons.north_east_rounded,
|
||||
metric: upMetric,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDirectionalSignal(
|
||||
BuildContext context, {
|
||||
required IconData icon,
|
||||
required SignalMetric metric,
|
||||
}) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icon,
|
||||
size: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 3),
|
||||
_buildMiniSignalBars(context, metric),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
metric.valueLabel,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.labelSmall?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniSignalBars(BuildContext context, SignalMetric metric) {
|
||||
final inactive = Theme.of(context).colorScheme.outlineVariant;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (var index = 0; index < 3; index++) ...[
|
||||
if (index > 0) const SizedBox(width: 2),
|
||||
Container(
|
||||
width: 3,
|
||||
height: 6.0 + (index * 4),
|
||||
decoration: BoxDecoration(
|
||||
color: index < metric.activeBars ? metric.color : inactive,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
@@ -6,17 +8,22 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:vibration/vibration.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import '../models/device_info.dart' show ConnectionMode;
|
||||
import '../models/contact.dart';
|
||||
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
|
||||
import '../providers/messages_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../theme/app_theme.dart';
|
||||
import 'messages_tab.dart';
|
||||
import 'contacts_tab.dart';
|
||||
import 'discovery_screen.dart';
|
||||
import 'sensors_tab.dart';
|
||||
import 'map_tab.dart';
|
||||
import 'repeaters_map_screen.dart';
|
||||
import 'settings_screen.dart';
|
||||
import 'device_config_screen.dart';
|
||||
import 'packet_log_screen.dart';
|
||||
import 'live_traffic_screen.dart';
|
||||
import 'profiles_screen.dart';
|
||||
import 'spectrum_scan_screen.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -24,6 +31,11 @@ import '../widgets/permission_request_dialog.dart';
|
||||
import '../widgets/connection_dialog.dart';
|
||||
import '../utils/battery_display_helper.dart';
|
||||
import '../services/developer_mode_service.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../services/profile_device_key_resolver.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../services/profiles_feature_service.dart';
|
||||
|
||||
enum _HomeTab { messages, contacts, sensors, map }
|
||||
|
||||
@@ -53,6 +65,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
with TickerProviderStateMixin, WidgetsBindingObserver {
|
||||
late TabController _tabController;
|
||||
late final AppProvider _appProvider;
|
||||
late final ConnectionProvider _connectionProvider;
|
||||
int _currentIndex = 0;
|
||||
bool _isMapFullscreen = false;
|
||||
bool _showRxTxIndicators = true;
|
||||
@@ -61,6 +74,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
bool _isContactsEnabled = true;
|
||||
bool _isSensorsEnabled = false;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
String? _lastProfileDeviceKey;
|
||||
|
||||
List<_HomeTab> get _enabledTabs {
|
||||
return [
|
||||
@@ -84,6 +98,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addObserver(this);
|
||||
_appProvider = context.read<AppProvider>();
|
||||
_connectionProvider = context.read<ConnectionProvider>();
|
||||
_connectionProvider.addListener(_handleConnectionProviderChanged);
|
||||
_isMapEnabled = _appProvider.isMapEnabled;
|
||||
_isContactsEnabled = _appProvider.isContactsEnabled;
|
||||
_isSensorsEnabled = _appProvider.isSensorsEnabled;
|
||||
@@ -93,6 +109,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
_initTabController();
|
||||
_loadRxTxPreference();
|
||||
_loadDeveloperModePreference();
|
||||
MeshMapNodesService.syncInBackgroundIfStale();
|
||||
|
||||
// Show permission dialog after the first frame if needed
|
||||
if (widget.shouldShowPermissionDialog) {
|
||||
@@ -100,6 +117,10 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
_showPermissionDialog();
|
||||
});
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_handleConnectionProviderChanged();
|
||||
});
|
||||
}
|
||||
|
||||
void _initTabController() {
|
||||
@@ -195,7 +216,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
|
||||
switch (tab) {
|
||||
case _HomeTab.messages:
|
||||
context.read<MessagesProvider>().markAllAsRead();
|
||||
break;
|
||||
case _HomeTab.contacts:
|
||||
context.read<ContactsProvider>().markAllAsViewed();
|
||||
@@ -216,17 +236,58 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
void _handleConnectionProviderChanged() {
|
||||
final deviceKey = ProfileDeviceKeyResolver.resolve(
|
||||
deviceInfo: _connectionProvider.deviceInfo,
|
||||
connectionMode: _connectionProvider.connectionMode,
|
||||
);
|
||||
if (_lastProfileDeviceKey == deviceKey) {
|
||||
return;
|
||||
}
|
||||
_lastProfileDeviceKey = deviceKey;
|
||||
if (deviceKey == null || !mounted) {
|
||||
return;
|
||||
}
|
||||
unawaited(
|
||||
context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.syncActiveProfileForCurrentDevice(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_connectionProvider.removeListener(_handleConnectionProviderChanged);
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
_appProvider.removeListener(_handleAppProviderChanged);
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _openLiveTraffic(ConnectionProvider provider) {
|
||||
openLiveTrafficScreen(context, provider);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_lifecycleState = state;
|
||||
_syncFastLocationUiState();
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
MeshMapNodesService.syncInBackgroundIfStale();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadRxTxPreference() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_showRxTxIndicators = prefs.getBool('show_rx_tx_indicators') ?? true;
|
||||
_showRxTxIndicators =
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
) ??
|
||||
true;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -239,16 +300,6 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
WidgetsBinding.instance.removeObserver(this);
|
||||
_appProvider.setFastLocationUiActive(false);
|
||||
_appProvider.removeListener(_handleAppProviderChanged);
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _showPermissionDialog() {
|
||||
if (!mounted) return;
|
||||
|
||||
@@ -344,7 +395,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.flood),
|
||||
subtitle: const Text('Relay through repeaters across the mesh'),
|
||||
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -370,7 +421,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
),
|
||||
title: Text(l10n.direct),
|
||||
subtitle: const Text('Nearby only, without repeater flooding'),
|
||||
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
|
||||
trailing: const Icon(Icons.chevron_right_rounded),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
@@ -384,34 +435,160 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityBadge({
|
||||
required String label,
|
||||
required int count,
|
||||
required bool isActive,
|
||||
required Color activeColor,
|
||||
}) {
|
||||
final color = isActive ? activeColor : Colors.grey;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.12),
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
void _showDeviceInfoSheet(BuildContext context, DeviceInfo deviceInfo) {
|
||||
// Request self telemetry so it's fresh
|
||||
context.read<ConnectionProvider>().requestSelfTelemetry();
|
||||
|
||||
// Find the device's own contact to show self telemetry
|
||||
final selfKey = deviceInfo.publicKey;
|
||||
Contact? selfContact;
|
||||
if (selfKey != null) {
|
||||
selfContact = context.read<ContactsProvider>().findContactByKey(
|
||||
Uint8List.fromList(selfKey),
|
||||
);
|
||||
}
|
||||
final telemetry = selfContact?.telemetry;
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 7,
|
||||
height: 7,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
||||
builder: (context) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 4,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).dividerColor,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
deviceInfo.selfName ?? deviceInfo.deviceName ?? 'Device',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bluetooth,
|
||||
'BLE Signal',
|
||||
deviceInfo.signalRssi != null
|
||||
? '${deviceInfo.signalRssi} dBm'
|
||||
: 'N/A',
|
||||
),
|
||||
if (deviceInfo.batteryPercent != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
BatteryDisplayHelper.getBatteryIcon(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
'Battery',
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
),
|
||||
if (deviceInfo.batteryMilliVolts != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.bolt,
|
||||
'Voltage',
|
||||
'${(deviceInfo.batteryMilliVolts! / 1000).toStringAsFixed(2)}V',
|
||||
),
|
||||
if (deviceInfo.storageUsedKb != null &&
|
||||
deviceInfo.storageTotalKb != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.storage,
|
||||
'Storage',
|
||||
'${deviceInfo.storageUsedKb} / ${deviceInfo.storageTotalKb} KB',
|
||||
),
|
||||
if (deviceInfo.firmwareVersion != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.system_update,
|
||||
'Firmware',
|
||||
'v${deviceInfo.firmwareVersion}',
|
||||
),
|
||||
if (deviceInfo.radioFreq != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.radio,
|
||||
'Frequency',
|
||||
'${(deviceInfo.radioFreq! / 1000).toStringAsFixed(3)} MHz',
|
||||
),
|
||||
if (deviceInfo.txPower != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.power,
|
||||
'TX Power',
|
||||
'${deviceInfo.txPower} dBm',
|
||||
),
|
||||
if (telemetry != null) ...[
|
||||
const Divider(height: 24),
|
||||
Text(
|
||||
'Self Telemetry',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (telemetry.temperature != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.thermostat,
|
||||
'Temperature',
|
||||
'${telemetry.temperature!.toStringAsFixed(1)}°C',
|
||||
),
|
||||
if (telemetry.humidity != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.water_drop,
|
||||
'Humidity',
|
||||
'${telemetry.humidity!.toStringAsFixed(1)}%',
|
||||
),
|
||||
if (telemetry.pressure != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.compress,
|
||||
'Pressure',
|
||||
'${telemetry.pressure!.toStringAsFixed(1)} hPa',
|
||||
),
|
||||
if (telemetry.gpsLocation != null)
|
||||
_deviceInfoRow(
|
||||
context,
|
||||
Icons.gps_fixed,
|
||||
'GPS',
|
||||
'${telemetry.gpsLocation!.latitude.toStringAsFixed(5)}, ${telemetry.gpsLocation!.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _deviceInfoRow(
|
||||
BuildContext context,
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, size: 18, color: Theme.of(context).colorScheme.primary),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(label, style: Theme.of(context).textTheme.bodyMedium),
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'$label:$count',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: color,
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -419,6 +596,27 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMiniSignalBars({required int activeBars, required Color color}) {
|
||||
final inactive = Colors.grey.withValues(alpha: 0.3);
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
for (var i = 0; i < 5; i++) ...[
|
||||
if (i > 0) const SizedBox(width: 1.5),
|
||||
Container(
|
||||
width: 2.5,
|
||||
height: 4.0 + (i * 2),
|
||||
decoration: BoxDecoration(
|
||||
color: i < activeBars ? color : inactive,
|
||||
borderRadius: BorderRadius.circular(1.5),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactActivityIndicator({
|
||||
required bool rxActive,
|
||||
required bool txActive,
|
||||
@@ -575,6 +773,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
}
|
||||
|
||||
context.watch<AppProvider>();
|
||||
final activeProfileId = context.select<ProfileManager, String>(
|
||||
(manager) => manager.activeProfileId,
|
||||
);
|
||||
|
||||
final enabledTabs = _enabledTabs;
|
||||
final isMapTabActive = _currentTab == _HomeTab.map;
|
||||
@@ -599,7 +800,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onPressed: () async {
|
||||
await provider.disconnect();
|
||||
},
|
||||
icon: const Icon(Icons.power_settings_new),
|
||||
icon: Icon(Icons.power_settings_new),
|
||||
tooltip: AppLocalizations.of(context)!.disconnect,
|
||||
color: Colors.red.shade700,
|
||||
);
|
||||
@@ -611,6 +812,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
icon: const Icon(Icons.more_vert),
|
||||
itemBuilder: (context) {
|
||||
final items = <PopupMenuEntry<void>>[];
|
||||
final profilesEnabled = context
|
||||
.read<ProfileManager>()
|
||||
.profilesEnabled;
|
||||
|
||||
if (_isDeveloperModeEnabled) {
|
||||
items.add(
|
||||
@@ -618,8 +822,8 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.radar),
|
||||
const SizedBox(width: 8),
|
||||
const Text('Spectrum Scan'),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.spectrumScan),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
@@ -642,8 +846,101 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
const Icon(Icons.radar_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.liveTraffic),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
final provider = context.read<ConnectionProvider>();
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => LiveTrafficScreen.fromProvider(
|
||||
provider,
|
||||
openPacketLogs: () {
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => PacketLogScreen(
|
||||
bleService: provider.bleService,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.router_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const RepeatersMapScreen(),
|
||||
fullscreenDialog: true,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.person_search),
|
||||
const SizedBox(width: 8),
|
||||
Consumer<ContactsProvider>(
|
||||
builder: (context, contactsProvider, child) {
|
||||
final pendingCount =
|
||||
contactsProvider.pendingAdverts.length;
|
||||
return Text(
|
||||
pendingCount > 0
|
||||
? 'Discovery ($pendingCount)'
|
||||
: 'Discovery',
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const DiscoveryScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.settings),
|
||||
SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.settings),
|
||||
],
|
||||
),
|
||||
@@ -668,6 +965,31 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
|
||||
if (profilesEnabled) {
|
||||
items.add(
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.layers_outlined),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.profiles),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
final navigator = Navigator.of(context);
|
||||
Future.delayed(Duration.zero, () {
|
||||
if (!mounted) return;
|
||||
navigator.push(
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const ProfilesScreen(),
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return items;
|
||||
},
|
||||
),
|
||||
@@ -679,6 +1001,9 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
switch (tab) {
|
||||
case _HomeTab.messages:
|
||||
return MessagesTab(
|
||||
isActive:
|
||||
_currentTab == _HomeTab.messages &&
|
||||
_lifecycleState == AppLifecycleState.resumed,
|
||||
onNavigateToMap: _isMapEnabled
|
||||
? () => _navigateToTab(_HomeTab.map)
|
||||
: null,
|
||||
@@ -691,9 +1016,14 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
return const SensorsTab();
|
||||
return SensorsTab(
|
||||
isActive:
|
||||
_currentTab == _HomeTab.sensors &&
|
||||
_lifecycleState == AppLifecycleState.resumed,
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return MapTab(
|
||||
key: ValueKey<String>('map:$activeProfileId'),
|
||||
onFullscreenChanged: (isFullscreen) {
|
||||
setState(() {
|
||||
_isMapFullscreen = isFullscreen;
|
||||
@@ -738,7 +1068,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
Icons.message,
|
||||
unreadCount,
|
||||
),
|
||||
text: AppLocalizations.of(context)!.messages,
|
||||
text: 'Chat',
|
||||
);
|
||||
case _HomeTab.contacts:
|
||||
return Tab(
|
||||
@@ -750,7 +1080,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
);
|
||||
case _HomeTab.map:
|
||||
return Tab(
|
||||
icon: const Icon(Icons.map),
|
||||
icon: Icon(Icons.map),
|
||||
text: AppLocalizations.of(context)!.map,
|
||||
);
|
||||
case _HomeTab.sensors:
|
||||
@@ -775,19 +1105,35 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
||||
|
||||
if (!isConnected) {
|
||||
// Disconnected state: show connect button
|
||||
final buttonLabel = provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
AppLocalizations.of(context)!.appTitle,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
provider.isReconnecting
|
||||
? 'Restoring previous link'
|
||||
: 'No device connected',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
ElevatedButton.icon(
|
||||
FilledButton.icon(
|
||||
onPressed: provider.isReconnecting
|
||||
? null
|
||||
: () => _showConnectionDialog(context),
|
||||
@@ -798,22 +1144,19 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Colors.black54,
|
||||
Colors.white70,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Icon(Icons.bluetooth, size: 18),
|
||||
label: Text(
|
||||
provider.isReconnecting
|
||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||
: AppLocalizations.of(context)!.connect,
|
||||
),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.white,
|
||||
foregroundColor: Colors.black87,
|
||||
elevation: 0,
|
||||
: const Icon(Icons.add_link_rounded, size: 18),
|
||||
label: Text(buttonLabel),
|
||||
style: FilledButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -821,7 +1164,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
const SizedBox(width: 8),
|
||||
IconButton(
|
||||
onPressed: () => provider.cancelReconnection(),
|
||||
icon: const Icon(Icons.close, size: 20),
|
||||
icon: Icon(Icons.close, size: 20),
|
||||
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
||||
style: IconButton.styleFrom(
|
||||
backgroundColor: Colors.red.shade700,
|
||||
@@ -880,7 +1223,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Row(
|
||||
GestureDetector(
|
||||
onTap: () => _showDeviceInfoSheet(
|
||||
context,
|
||||
deviceInfo,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
@@ -893,17 +1241,12 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
if (!isTcpConnected &&
|
||||
deviceInfo.signalRssi != null) ...[
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${deviceInfo.signalRssi}',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: signalColor,
|
||||
),
|
||||
maxLines: 1,
|
||||
),
|
||||
_buildMiniSignalBars(
|
||||
activeBars:
|
||||
BatteryDisplayHelper.getSignalBars(
|
||||
deviceInfo.signalRssi!,
|
||||
),
|
||||
color: signalColor,
|
||||
),
|
||||
],
|
||||
if (deviceInfo.batteryPercent != null) ...[
|
||||
@@ -918,25 +1261,22 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
SizedBox(
|
||||
width: 30,
|
||||
child: Text(
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 2),
|
||||
Text(
|
||||
'${deviceInfo.batteryPercent!.round()}%',
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color:
|
||||
BatteryDisplayHelper.getBatteryColor(
|
||||
deviceInfo.batteryPercent!,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -1022,6 +1362,7 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
SizedBox(width: isTight ? 8 : 12),
|
||||
if (_showRxTxIndicators)
|
||||
GestureDetector(
|
||||
onTap: () => _openLiveTraffic(provider),
|
||||
onLongPress: () {
|
||||
Navigator.push(
|
||||
context,
|
||||
@@ -1031,44 +1372,13 @@ class _HomeScreenState extends State<HomeScreen>
|
||||
),
|
||||
);
|
||||
},
|
||||
child: isTight
|
||||
? _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
)
|
||||
: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 8,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.colorScheme.surfaceContainerHigh
|
||||
.withValues(alpha: 0.85),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
_buildActivityBadge(
|
||||
label: 'RX',
|
||||
count: provider.rxPacketCount,
|
||||
isActive: provider.rxActivity,
|
||||
activeColor: Colors.green,
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
_buildActivityBadge(
|
||||
label: 'TX',
|
||||
count: provider.txPacketCount,
|
||||
isActive: provider.txActivity,
|
||||
activeColor: Colors.blue,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: _buildCompactActivityIndicator(
|
||||
rxActive: provider.rxActivity,
|
||||
txActive: provider.txActivity,
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(width: isTight ? 24 : 74),
|
||||
const SizedBox(width: 24),
|
||||
],
|
||||
);
|
||||
},
|
||||
|
||||
1154
lib/screens/live_traffic_screen.dart
Normal file
1154
lib/screens/live_traffic_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,7 @@ import '../services/voice_recorder_service.dart';
|
||||
import '../services/voice_codec_service.dart';
|
||||
import '../utils/toast_logger.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import '../utils/contact_sorting.dart';
|
||||
import '../utils/voice_message_parser.dart';
|
||||
import '../utils/image_message_parser.dart';
|
||||
import '../utils/tictactoe_message_parser.dart';
|
||||
@@ -39,8 +40,9 @@ import '../l10n/app_localizations.dart';
|
||||
|
||||
class MessagesTab extends StatefulWidget {
|
||||
final VoidCallback? onNavigateToMap;
|
||||
final bool isActive;
|
||||
|
||||
const MessagesTab({super.key, this.onNavigateToMap});
|
||||
const MessagesTab({super.key, this.onNavigateToMap, this.isActive = true});
|
||||
|
||||
@override
|
||||
State<MessagesTab> createState() => _MessagesTabState();
|
||||
@@ -50,6 +52,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
static const int _maxContactMessageBytes = 156;
|
||||
static const int _maxChannelMessageBytes = 127;
|
||||
static const double _composerOverlayHeight = 148;
|
||||
static const Duration _channelAutoReadDelay = Duration(seconds: 5);
|
||||
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final FocusNode _focusNode = FocusNode();
|
||||
@@ -57,6 +60,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
int _messageByteCount = 0;
|
||||
String? _highlightedMessageId;
|
||||
Timer? _highlightTimer; // Timer for clearing message highlight
|
||||
Timer? _channelReadTimer;
|
||||
String? _pendingChannelReadKey;
|
||||
bool _suppressMentionTrigger = false;
|
||||
TextRange? _activeMentionRange;
|
||||
String _mentionQuery = '';
|
||||
List<Contact> _mentionSuggestions = const [];
|
||||
|
||||
// Message destination state
|
||||
String _destinationType =
|
||||
@@ -71,7 +80,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final VoiceRecorderService _voiceRecorder = VoiceRecorderService();
|
||||
bool _isRecording = false;
|
||||
bool _isSendingVoice = false;
|
||||
static const int _maxVoicePackets = 10;
|
||||
static const Duration _maxVoiceRecordingDuration = Duration(seconds: 30);
|
||||
static const double _silenceRmsThreshold = 500.0;
|
||||
static const double _silencePeakThreshold = 1400.0;
|
||||
static const int _maxInteriorSilentChunks = 2;
|
||||
@@ -85,18 +94,17 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_textController.addListener(_updateCharacterCount);
|
||||
_textController.addListener(_handleComposerChanged);
|
||||
_focusNode.addListener(_handleFocusChanged);
|
||||
// Load saved message destination
|
||||
_loadSavedDestination();
|
||||
_loadVoiceBitrate();
|
||||
// Mark all messages as read when tab is opened
|
||||
_loadVoiceSettings();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
context.read<MessagesProvider>().markAllAsRead();
|
||||
_checkForNavigationRequest();
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadVoiceBitrate() async {
|
||||
Future<void> _loadVoiceSettings() async {
|
||||
final bitrate = await VoiceBitratePreferences.getBitrate();
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
@@ -117,14 +125,24 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
@override
|
||||
void dispose() {
|
||||
_highlightTimer?.cancel();
|
||||
_channelReadTimer?.cancel();
|
||||
_voiceStreamSub?.cancel();
|
||||
_voiceRecorder.dispose();
|
||||
_focusNode.removeListener(_handleFocusChanged);
|
||||
_textController.dispose();
|
||||
_focusNode.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant MessagesTab oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.isActive != widget.isActive) {
|
||||
_syncChannelAutoReadTimer(context.read<MessagesProvider>());
|
||||
}
|
||||
}
|
||||
|
||||
void _checkForNavigationRequest() {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
final targetMessageId = messagesProvider.targetMessageId;
|
||||
@@ -146,9 +164,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
|
||||
void _scrollToMessage(String messageId) {
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
final messages = _getFilteredMessages(messagesProvider);
|
||||
final messages = messagesProvider.buildDisplayMessages(
|
||||
_getFilteredMessages(messagesProvider),
|
||||
);
|
||||
|
||||
final messageIndex = messages.indexWhere((m) => m.id == messageId);
|
||||
final messageIndex = messages.indexWhere(
|
||||
(entry) => entry.message.id == messageId,
|
||||
);
|
||||
|
||||
if (messageIndex != -1 && _scrollController.hasClients) {
|
||||
// Calculate position - accounting for reverse list
|
||||
@@ -179,12 +201,130 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleComposerChanged() {
|
||||
_updateCharacterCount();
|
||||
|
||||
if (_suppressMentionTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateMentionSuggestions(_textController.value);
|
||||
}
|
||||
|
||||
void _updateCharacterCount() {
|
||||
setState(() {
|
||||
_messageByteCount = utf8.encode(_textController.text).length;
|
||||
});
|
||||
}
|
||||
|
||||
void _handleFocusChanged() {
|
||||
if (!_focusNode.hasFocus) {
|
||||
_clearMentionSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
if (_suppressMentionTrigger) {
|
||||
return;
|
||||
}
|
||||
|
||||
_updateMentionSuggestions(_textController.value);
|
||||
}
|
||||
|
||||
void _updateMentionSuggestions(TextEditingValue value) {
|
||||
final triggerRange = _getMentionTriggerRange(value);
|
||||
if (triggerRange == null) {
|
||||
_clearMentionSuggestions();
|
||||
return;
|
||||
}
|
||||
|
||||
final query = value.text.substring(
|
||||
triggerRange.start + 1,
|
||||
triggerRange.end,
|
||||
);
|
||||
final contacts = _buildMentionSuggestions(query);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_activeMentionRange = triggerRange;
|
||||
_mentionQuery = query;
|
||||
_mentionSuggestions = contacts;
|
||||
});
|
||||
}
|
||||
|
||||
void _clearMentionSuggestions() {
|
||||
if (_activeMentionRange == null &&
|
||||
_mentionQuery.isEmpty &&
|
||||
_mentionSuggestions.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_activeMentionRange = null;
|
||||
_mentionQuery = '';
|
||||
_mentionSuggestions = const [];
|
||||
});
|
||||
}
|
||||
|
||||
TextRange? _getMentionTriggerRange(TextEditingValue value) {
|
||||
if (!value.selection.isValid || !value.selection.isCollapsed) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final cursorOffset = value.selection.baseOffset;
|
||||
if (cursorOffset <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final text = value.text;
|
||||
final triggerStart = text.lastIndexOf('@', cursorOffset - 1);
|
||||
if (triggerStart == -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final leadingChar = triggerStart > 0 ? text[triggerStart - 1] : null;
|
||||
if (leadingChar != null && !RegExp(r'[\s\(\[\{]').hasMatch(leadingChar)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final query = text.substring(triggerStart + 1, cursorOffset);
|
||||
if (query.contains(RegExp(r'[\s@\[\]\n\r]'))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TextRange(start: triggerStart, end: cursorOffset);
|
||||
}
|
||||
|
||||
List<Contact> _buildMentionSuggestions(String query) {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final normalizedQuery = query.trim().toLowerCase();
|
||||
final contacts =
|
||||
contactsProvider.chatContacts.where((contact) {
|
||||
if (normalizedQuery.isEmpty) return true;
|
||||
return contact.displayName.toLowerCase().contains(normalizedQuery);
|
||||
}).toList()..sort((a, b) {
|
||||
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
final aName = a.displayName.toLowerCase();
|
||||
final bName = b.displayName.toLowerCase();
|
||||
final aStarts =
|
||||
normalizedQuery.isNotEmpty && aName.startsWith(normalizedQuery);
|
||||
final bStarts =
|
||||
normalizedQuery.isNotEmpty && bName.startsWith(normalizedQuery);
|
||||
if (aStarts != bStarts) {
|
||||
return aStarts ? -1 : 1;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
|
||||
return contacts.take(8).toList(growable: false);
|
||||
}
|
||||
|
||||
int get _maxMessageBytes =>
|
||||
_destinationType == MessageDestinationPreferences.destinationTypeChannel
|
||||
? _maxChannelMessageBytes
|
||||
@@ -269,16 +409,33 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
final messagesProvider = context.read<MessagesProvider>();
|
||||
|
||||
// Filter contacts by type
|
||||
final contacts = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.chat)
|
||||
.toList();
|
||||
final rooms = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.room)
|
||||
.toList();
|
||||
final channels = contactsProvider.contacts
|
||||
.where((c) => c.type == ContactType.channel)
|
||||
.toList();
|
||||
final contacts = List<Contact>.from(contactsProvider.chatContacts)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
final rooms = List<Contact>.from(contactsProvider.rooms)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
final channels = List<Contact>.from(contactsProvider.channels)
|
||||
..sort((a, b) {
|
||||
final primary = compareContactsByLastSeen(a, b);
|
||||
if (primary != 0) {
|
||||
return primary;
|
||||
}
|
||||
|
||||
return compareContactsByDisplayName(a, b);
|
||||
});
|
||||
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
@@ -309,6 +466,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
_selectedRecipient = recipient;
|
||||
});
|
||||
|
||||
_markCurrentDestinationAsRead();
|
||||
|
||||
_enforceMessageByteLimit();
|
||||
|
||||
// Save to preferences
|
||||
@@ -338,9 +497,46 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
void _insertReplyMention(String displayName, {TextRange? replacementRange}) {
|
||||
final trimmedName = displayName.trim();
|
||||
if (trimmedName.isEmpty) return;
|
||||
|
||||
final mention = '@[$trimmedName] ';
|
||||
final value = _textController.value;
|
||||
final selection = replacementRange ?? value.selection;
|
||||
final hasSelection =
|
||||
selection.isValid &&
|
||||
selection.start >= 0 &&
|
||||
selection.end >= selection.start;
|
||||
|
||||
final start = hasSelection ? selection.start : value.text.length;
|
||||
final end = hasSelection ? selection.end : value.text.length;
|
||||
final nextText = value.text.replaceRange(start, end, mention);
|
||||
final nextOffset = start + mention.length;
|
||||
|
||||
_suppressMentionTrigger = true;
|
||||
_textController.value = value.copyWith(
|
||||
text: nextText,
|
||||
selection: TextSelection.collapsed(offset: nextOffset),
|
||||
composing: TextRange.empty,
|
||||
);
|
||||
_suppressMentionTrigger = false;
|
||||
_clearMentionSuggestions();
|
||||
_enforceMessageByteLimit();
|
||||
}
|
||||
|
||||
void _selectMention(Contact contact) {
|
||||
_insertReplyMention(
|
||||
contact.displayName,
|
||||
replacementRange: _activeMentionRange,
|
||||
);
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
Future<void> _replyToMessage(Message message) async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final contactsProvider = context.read<ContactsProvider>();
|
||||
Contact? senderContact;
|
||||
String? senderDisplayName;
|
||||
|
||||
String destinationType;
|
||||
Contact? recipient;
|
||||
@@ -356,30 +552,67 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}).firstOrNull;
|
||||
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
final senderPrefix = message.senderPublicKeyPrefix;
|
||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
||||
ToastLogger.error(context, l10n.cannotReplySenderMissing);
|
||||
return;
|
||||
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
}
|
||||
senderDisplayName =
|
||||
senderContact?.displayName ?? message.senderName?.trim();
|
||||
} else {
|
||||
final roomRecipient = message.recipientPublicKey == null
|
||||
? null
|
||||
: contactsProvider.findContactByKey(message.recipientPublicKey!);
|
||||
|
||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
||||
return;
|
||||
if (roomRecipient?.isRoom == true) {
|
||||
destinationType = MessageDestinationPreferences.destinationTypeRoom;
|
||||
recipient = roomRecipient;
|
||||
|
||||
final senderPrefix = message.senderPublicKeyPrefix;
|
||||
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
}
|
||||
senderDisplayName =
|
||||
senderContact?.displayName ?? message.senderName?.trim();
|
||||
} else {
|
||||
final senderPrefix = message.senderPublicKeyPrefix;
|
||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.cannotReplySenderMissing,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
||||
if (recipient == null) {
|
||||
ToastLogger.error(
|
||||
context,
|
||||
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
destinationType = recipient.isRoom
|
||||
? MessageDestinationPreferences.destinationTypeRoom
|
||||
: MessageDestinationPreferences.destinationTypeContact;
|
||||
}
|
||||
|
||||
destinationType = recipient.isRoom
|
||||
? MessageDestinationPreferences.destinationTypeRoom
|
||||
: MessageDestinationPreferences.destinationTypeContact;
|
||||
}
|
||||
|
||||
await _onRecipientSelected(destinationType, recipient);
|
||||
if (!mounted) return;
|
||||
if ((message.isChannelMessage || recipient?.isRoom == true) &&
|
||||
senderDisplayName != null &&
|
||||
senderDisplayName.isNotEmpty) {
|
||||
_insertReplyMention(senderDisplayName);
|
||||
}
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@@ -433,6 +666,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
return;
|
||||
}
|
||||
|
||||
_textController.clear();
|
||||
_focusNode.unfocus();
|
||||
|
||||
try {
|
||||
// Check destination type and send accordingly
|
||||
if (_destinationType ==
|
||||
@@ -463,12 +699,18 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
await _sendToChannel(text, connectionProvider, messagesProvider, 0);
|
||||
}
|
||||
|
||||
_textController.clear();
|
||||
_focusNode.unfocus();
|
||||
_markCurrentDestinationAsRead();
|
||||
|
||||
if (!mounted) return;
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
if (_textController.text.isEmpty) {
|
||||
_textController.text = text;
|
||||
_textController.selection = TextSelection.collapsed(
|
||||
offset: _textController.text.length,
|
||||
);
|
||||
_focusNode.requestFocus();
|
||||
}
|
||||
ToastLogger.error(context, 'Failed to send: $e');
|
||||
}
|
||||
}
|
||||
@@ -782,7 +1024,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
Future<void> _startVoiceRecording() async {
|
||||
if (_isSendingVoice || _isRecording) return;
|
||||
debugPrint('🎙️ [Voice] _startVoiceRecording called');
|
||||
// Read fresh bitrate preference so settings changes apply immediately.
|
||||
// Read fresh voice preferences so settings changes apply immediately.
|
||||
final selectedBitrate = await VoiceBitratePreferences.getBitrate();
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
@@ -821,11 +1063,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
_selectedVoiceBitrate,
|
||||
);
|
||||
final packetDuration = Duration(
|
||||
milliseconds: codec2ModeFor(_activeVoiceMode!).packetDurationMs,
|
||||
milliseconds: _activeVoiceMode!.packetDurationMs,
|
||||
);
|
||||
final maxVoicePackets = _maxVoicePacketsForMode(_activeVoiceMode!);
|
||||
|
||||
debugPrint(
|
||||
'🎙️ [Voice] session=$_currentVoiceSessionId mode=$_activeVoiceMode chunkDuration=${packetDuration.inMilliseconds}ms',
|
||||
'🎙️ [Voice] session=$_currentVoiceSessionId mode=$_activeVoiceMode chunkDuration=${packetDuration.inMilliseconds}ms maxPackets=$maxVoicePackets',
|
||||
);
|
||||
|
||||
_recordedChunks.clear();
|
||||
@@ -834,9 +1077,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
try {
|
||||
final stream = _voiceRecorder.startCapture(
|
||||
chunkDuration: packetDuration,
|
||||
sampleRateHz: _activeVoiceMode!.sampleRateHz,
|
||||
enableBandPassFilter: appProvider.isVoiceBandPassFilterEnabled,
|
||||
enableCompressor: appProvider.isVoiceCompressorEnabled,
|
||||
enableLimiter: appProvider.isVoiceLimiterEnabled,
|
||||
enableAutoGain: appProvider.isVoiceAutoGainEnabled,
|
||||
enableEchoCancellation: appProvider.isVoiceEchoCancellationEnabled,
|
||||
enableNoiseSuppression: appProvider.isVoiceNoiseSuppressionEnabled,
|
||||
);
|
||||
debugPrint('🎙️ [Voice] capture started, listening for chunks...');
|
||||
_voiceStreamSub = stream.listen(
|
||||
@@ -847,7 +1094,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
'🎙️ [Voice] chunk #${_recordedChunks.length} received: ${pcmChunk.length} samples',
|
||||
);
|
||||
setState(() {});
|
||||
if (_recordedChunks.length >= _maxVoicePackets) {
|
||||
if (_recordedChunks.length >= maxVoicePackets) {
|
||||
debugPrint('🎙️ [Voice] max packets reached, stopping');
|
||||
_stopAndSendVoice();
|
||||
}
|
||||
@@ -864,6 +1111,12 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
int _maxVoicePacketsForMode(VoicePacketMode mode) {
|
||||
final packets =
|
||||
_maxVoiceRecordingDuration.inMilliseconds ~/ mode.packetDurationMs;
|
||||
return packets < 1 ? 1 : packets;
|
||||
}
|
||||
|
||||
Future<void> _stopAndSendVoice() async {
|
||||
if (!_isRecording) return;
|
||||
final trimSilenceEnabled = context
|
||||
@@ -878,9 +1131,14 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
await _voiceRecorder.stopCapture();
|
||||
|
||||
final rawChunks = List<Int16List>.from(_recordedChunks);
|
||||
final chunks = trimSilenceEnabled ? _trimSilence(rawChunks) : rawChunks;
|
||||
final trimmedChunks = trimSilenceEnabled
|
||||
? _trimSilence(rawChunks)
|
||||
: rawChunks;
|
||||
final sessionId = _currentVoiceSessionId;
|
||||
final mode = _activeVoiceMode;
|
||||
final chunks = mode == null
|
||||
? trimmedChunks
|
||||
: _prepareChunksForSending(trimmedChunks, mode);
|
||||
_recordedChunks.clear();
|
||||
|
||||
debugPrint(
|
||||
@@ -1069,6 +1327,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
messagesProvider.markMessageSent(msgId, 0, 0);
|
||||
}
|
||||
|
||||
List<Int16List> _prepareChunksForSending(
|
||||
List<Int16List> chunks,
|
||||
VoicePacketMode mode,
|
||||
) {
|
||||
return chunks;
|
||||
}
|
||||
|
||||
List<Int16List> _trimSilence(List<Int16List> chunks) {
|
||||
if (chunks.isEmpty) return chunks;
|
||||
|
||||
@@ -1098,16 +1363,28 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
bool _isSilentChunk(Int16List chunk) {
|
||||
if (chunk.isEmpty) return true;
|
||||
|
||||
final rms = _chunkRms(chunk);
|
||||
final peak = _chunkPeak(chunk);
|
||||
return rms < _silenceRmsThreshold && peak < _silencePeakThreshold;
|
||||
}
|
||||
|
||||
double _chunkRms(Int16List chunk) {
|
||||
var sumSquares = 0.0;
|
||||
for (final sample in chunk) {
|
||||
sumSquares += sample * sample;
|
||||
}
|
||||
return math.sqrt(sumSquares / chunk.length);
|
||||
}
|
||||
|
||||
int _chunkPeak(Int16List chunk) {
|
||||
var peak = 0;
|
||||
for (final sample in chunk) {
|
||||
final absSample = sample.abs();
|
||||
if (absSample > peak) peak = absSample;
|
||||
sumSquares += sample * sample;
|
||||
if (absSample > peak) {
|
||||
peak = absSample;
|
||||
}
|
||||
}
|
||||
|
||||
final rms = math.sqrt(sumSquares / chunk.length);
|
||||
return rms < _silenceRmsThreshold && peak < _silencePeakThreshold;
|
||||
return peak;
|
||||
}
|
||||
|
||||
bool _isPublicChannelSelected() {
|
||||
@@ -1125,7 +1402,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
final decision = await showDialog<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertDialog(
|
||||
title: const Text('Send to Public Channel?'),
|
||||
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
|
||||
content: Text(
|
||||
'You are about to send $mediaType to the Public Channel. '
|
||||
'This is not advised because everyone on the mesh may receive it. '
|
||||
@@ -1138,7 +1415,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||
child: const Text('Send anyway'),
|
||||
child: Text(AppLocalizations.of(context)!.sendAnyway),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -1198,8 +1475,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.search),
|
||||
title: const Text('Search messages'),
|
||||
leading: Icon(Icons.search),
|
||||
title: Text(AppLocalizations.of(context)!.searchMessages),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
_showFilteredMessageSearch();
|
||||
@@ -1207,7 +1484,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.add_location_alt),
|
||||
leading: Icon(Icons.add_location_alt),
|
||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
@@ -1234,8 +1511,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.photo_library),
|
||||
title: const Text('Send image from gallery'),
|
||||
leading: Icon(Icons.photo_library),
|
||||
title: Text(AppLocalizations.of(context)!.sendImageFromGallery),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
@@ -1246,8 +1523,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
),
|
||||
ListTile(
|
||||
enabled: !_isSendingImage,
|
||||
leading: const Icon(Icons.camera_alt),
|
||||
title: const Text('Take photo'),
|
||||
leading: Icon(Icons.camera_alt),
|
||||
title: Text(AppLocalizations.of(context)!.takePhoto),
|
||||
onTap: _isSendingImage
|
||||
? null
|
||||
: () async {
|
||||
@@ -1257,9 +1534,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.grid_3x3),
|
||||
title: const Text('Start Tic-Tac-Toe'),
|
||||
subtitle: const Text('DM only'),
|
||||
leading: Icon(Icons.grid_3x3),
|
||||
title: Text(AppLocalizations.of(context)!.startTictactoe),
|
||||
subtitle: Text(AppLocalizations.of(context)!.dmOnly),
|
||||
onTap: () async {
|
||||
await _runAfterSheetDismissal(sheetContext, () async {
|
||||
await _startTicTacToeGame();
|
||||
@@ -1641,6 +1918,65 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
}
|
||||
}
|
||||
|
||||
void _markCurrentDestinationAsRead() {
|
||||
_channelReadTimer?.cancel();
|
||||
_pendingChannelReadKey = null;
|
||||
context.read<MessagesProvider>().markDestinationAsRead(
|
||||
destinationType: _destinationType,
|
||||
contact: _selectedRecipient,
|
||||
);
|
||||
}
|
||||
|
||||
void _syncChannelAutoReadTimer(MessagesProvider messagesProvider) {
|
||||
if (!widget.isActive ||
|
||||
_destinationType !=
|
||||
MessageDestinationPreferences.destinationTypeChannel) {
|
||||
_channelReadTimer?.cancel();
|
||||
_pendingChannelReadKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final channelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||
final unreadCount = messagesProvider.getUnreadCountForChannel(channelIdx);
|
||||
|
||||
if (unreadCount <= 0) {
|
||||
_channelReadTimer?.cancel();
|
||||
_pendingChannelReadKey = null;
|
||||
return;
|
||||
}
|
||||
|
||||
final nextKey = '$channelIdx:$unreadCount';
|
||||
if (_pendingChannelReadKey == nextKey &&
|
||||
_channelReadTimer?.isActive == true) {
|
||||
return;
|
||||
}
|
||||
|
||||
_channelReadTimer?.cancel();
|
||||
_pendingChannelReadKey = nextKey;
|
||||
_channelReadTimer = Timer(_channelAutoReadDelay, () {
|
||||
if (!mounted || !widget.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_destinationType !=
|
||||
MessageDestinationPreferences.destinationTypeChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
final currentChannelIdx = _selectedRecipient?.publicKey[1] ?? 0;
|
||||
if (currentChannelIdx != channelIdx) {
|
||||
return;
|
||||
}
|
||||
|
||||
final latestProvider = context.read<MessagesProvider>();
|
||||
if (latestProvider.getUnreadCountForChannel(channelIdx) <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
_markCurrentDestinationAsRead();
|
||||
});
|
||||
}
|
||||
|
||||
List<Message> _getFilteredMessages(MessagesProvider messagesProvider) {
|
||||
// Get all recent messages
|
||||
final allMessages = messagesProvider.getRecentMessages(count: 100);
|
||||
@@ -1710,12 +2046,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
void _handleMessageTap(Message message) {
|
||||
if (widget.onNavigateToMap == null) return;
|
||||
|
||||
if (message.isSarMarker && message.sarGpsCoordinates != null) {
|
||||
if (message.isSarMarker) {
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
mapProvider.navigateToLocation(
|
||||
location: message.sarGpsCoordinates!,
|
||||
zoom: 15.0,
|
||||
);
|
||||
final marker = message.toSarMarker();
|
||||
if (marker == null) {
|
||||
return;
|
||||
}
|
||||
final error = mapProvider.navigateToSarMarker(marker);
|
||||
if (error != null && mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
return;
|
||||
}
|
||||
widget.onNavigateToMap?.call();
|
||||
return;
|
||||
}
|
||||
@@ -1724,7 +2067,16 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
|
||||
final mapProvider = context.read<MapProvider>();
|
||||
final drawingProvider = context.read<DrawingProvider>();
|
||||
mapProvider.navigateToDrawing(message.drawingId!, drawingProvider);
|
||||
final error = mapProvider.navigateToDrawing(
|
||||
message.drawingId!,
|
||||
drawingProvider,
|
||||
);
|
||||
if (error != null && mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(error)));
|
||||
return;
|
||||
}
|
||||
widget.onNavigateToMap?.call();
|
||||
}
|
||||
}
|
||||
@@ -1733,7 +2085,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<MessagesProvider>(
|
||||
builder: (context, messagesProvider, child) {
|
||||
final messages = _getFilteredMessages(messagesProvider);
|
||||
_syncChannelAutoReadTimer(messagesProvider);
|
||||
final messages = messagesProvider.buildDisplayMessages(
|
||||
_getFilteredMessages(messagesProvider),
|
||||
);
|
||||
final bottomInset = MediaQuery.of(context).viewPadding.bottom;
|
||||
final composerBottomPadding = bottomInset > 0 ? 2.0 : 10.0;
|
||||
|
||||
@@ -1771,6 +2126,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
||||
bottomPadding: composerBottomPadding,
|
||||
destinationLabel: _getDestinationLabel(),
|
||||
destinationAvatar: _buildDestinationAvatar(context),
|
||||
mentionSuggestions: _mentionSuggestions,
|
||||
mentionQuery: _mentionQuery,
|
||||
onMentionSelected: _selectMention,
|
||||
onShowComposerActions: _showComposerActions,
|
||||
onShowRecipientSelector: _showRecipientSelector,
|
||||
onStartVoiceRecording: _startVoiceRecording,
|
||||
|
||||
@@ -8,6 +8,7 @@ import 'package:meshcore_client/meshcore_client.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../services/route_hash_preferences.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
@@ -60,7 +61,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -107,7 +108,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
||||
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -154,8 +155,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
||||
Clipboard.setData(ClipboardData(text: log.hexData));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Hex data copied to clipboard'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.hexDataCopiedToClipboard),
|
||||
duration: Duration(seconds: 1),
|
||||
),
|
||||
);
|
||||
@@ -183,7 +184,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
setState(() {});
|
||||
if (parentContext.mounted) {
|
||||
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||
const SnackBar(content: Text('Packet logs cleared')),
|
||||
SnackBar(content: Text(AppLocalizations.of(context)!.packetLogsCleared)),
|
||||
);
|
||||
}
|
||||
},
|
||||
@@ -204,7 +205,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
title: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text('BLE Packet Logs'),
|
||||
Text(AppLocalizations.of(context)!.blePacketLogs),
|
||||
Text(
|
||||
'${logs.length} packets',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
@@ -315,23 +316,23 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
icon: const Icon(Icons.share),
|
||||
tooltip: 'Export logs',
|
||||
itemBuilder: (context) => [
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'csv',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.table_chart),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as CSV'),
|
||||
const Icon(Icons.table_chart),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsCsv),
|
||||
],
|
||||
),
|
||||
),
|
||||
const PopupMenuItem(
|
||||
PopupMenuItem(
|
||||
value: 'txt',
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(Icons.text_snippet),
|
||||
SizedBox(width: 8),
|
||||
Text('Export as Text'),
|
||||
const Icon(Icons.text_snippet),
|
||||
const SizedBox(width: 8),
|
||||
Text(AppLocalizations.of(context)!.exportAsText),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -410,8 +411,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
||||
_filterDirection = null;
|
||||
});
|
||||
},
|
||||
icon: const Icon(Icons.clear_all),
|
||||
label: const Text('Clear filters'),
|
||||
icon: Icon(Icons.clear_all),
|
||||
label: Text(AppLocalizations.of(context)!.clearFilters),
|
||||
),
|
||||
],
|
||||
],
|
||||
@@ -518,24 +519,24 @@ class _PacketLogCard extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: isRx ? Icons.call_received : Icons.call_made,
|
||||
label: 'Direction',
|
||||
label: AppLocalizations.of(context)!.direction,
|
||||
value: isRx ? 'RX' : 'TX',
|
||||
accent: directionColor,
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.data_object,
|
||||
label: 'Size',
|
||||
label: AppLocalizations.of(context)!.size,
|
||||
value: '${log.rawData.length} bytes',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.schedule,
|
||||
label: 'Captured',
|
||||
label: AppLocalizations.of(context)!.captured,
|
||||
value: _formatTimestamp(log.timestamp),
|
||||
),
|
||||
if (log.responseCode != null)
|
||||
_FactCard(
|
||||
icon: Icons.sell,
|
||||
label: 'Opcode',
|
||||
label: AppLocalizations.of(context)!.opcode,
|
||||
value: log.opcodeName,
|
||||
),
|
||||
],
|
||||
@@ -564,7 +565,7 @@ class _PacketLogCard extends StatelessWidget {
|
||||
const SizedBox(height: 10),
|
||||
if (rxInfo?.rssiDbm != null)
|
||||
_SignalMeter(
|
||||
label: 'RSSI',
|
||||
label: AppLocalizations.of(context)!.rssi,
|
||||
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
||||
normalized: _normalizeRssi(
|
||||
rxInfo.rssiDbm!.toDouble(),
|
||||
@@ -572,9 +573,9 @@ class _PacketLogCard extends StatelessWidget {
|
||||
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
||||
),
|
||||
if (rxInfo?.snrDb != null) ...[
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
_SignalMeter(
|
||||
label: 'SNR',
|
||||
label: AppLocalizations.of(context)!.snr,
|
||||
valueLabel:
|
||||
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
||||
normalized: _normalizeSnr(rxInfo.snrDb!),
|
||||
@@ -746,49 +747,82 @@ class _DecodedRouteSection extends StatelessWidget {
|
||||
connectionProvider.deviceInfo.selfName ??
|
||||
connectionProvider.deviceInfo.displayName;
|
||||
|
||||
return FutureBuilder<int>(
|
||||
future: RouteHashPreferences.getHashSize(),
|
||||
return FutureBuilder<List<dynamic>>(
|
||||
future: Future.wait<dynamic>([
|
||||
RouteHashPreferences.getHashSize(),
|
||||
MeshMapNodesService.loadCachedNodes(
|
||||
cacheTtl: MeshMapNodesService.traceCacheTtl,
|
||||
),
|
||||
]),
|
||||
builder: (context, snapshot) {
|
||||
final decodedRoute = LogRxRouteDecoder.decode(
|
||||
log.rawData,
|
||||
preferredHashSize: snapshot.data,
|
||||
preferredHashSize: snapshot.data?.first as int?,
|
||||
);
|
||||
if (decodedRoute == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final cachedNodes = snapshot.hasData
|
||||
? snapshot.data![1] as List<MeshMapNode>
|
||||
: const <MeshMapNode>[];
|
||||
final resolvedPath = decodedRoute.hopHashes
|
||||
.map(
|
||||
(hashHex) => LogRxRouteDecoder.resolveHash(
|
||||
(hashHex) => _resolveHashWithFallback(
|
||||
hashHex,
|
||||
contacts: contacts,
|
||||
cachedNodes: cachedNodes,
|
||||
ownPublicKey: ownPublicKey,
|
||||
ownName: ownName,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
final originalSender = resolvedPath.isEmpty ? null : resolvedPath.first;
|
||||
|
||||
return _RouteSection(
|
||||
route: decodedRoute,
|
||||
path: resolvedPath,
|
||||
originalSender: originalSender,
|
||||
);
|
||||
return _RouteSection(route: decodedRoute, path: resolvedPath);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
ResolvedNodeHash _resolveHashWithFallback(
|
||||
String hashHex, {
|
||||
required List<Contact> contacts,
|
||||
required List<MeshMapNode> cachedNodes,
|
||||
required Uint8List? ownPublicKey,
|
||||
required String? ownName,
|
||||
}) {
|
||||
final localResolved = LogRxRouteDecoder.resolveHash(
|
||||
hashHex,
|
||||
contacts: contacts,
|
||||
ownPublicKey: ownPublicKey,
|
||||
ownName: ownName,
|
||||
);
|
||||
if (localResolved.matchCount > 0 || localResolved.isOwnNode) {
|
||||
return localResolved;
|
||||
}
|
||||
|
||||
final normalizedHashHex = hashHex.toLowerCase();
|
||||
final matches = cachedNodes
|
||||
.where((node) => node.publicKey.startsWith(normalizedHashHex))
|
||||
.toList();
|
||||
if (matches.isEmpty) {
|
||||
return localResolved;
|
||||
}
|
||||
|
||||
matches.sort((a, b) => b.updatedAtMs.compareTo(a.updatedAtMs));
|
||||
return ResolvedNodeHash(
|
||||
hashHex: normalizedHashHex,
|
||||
label: matches.first.name,
|
||||
isOwnNode: false,
|
||||
isUniqueMatch: matches.length == 1,
|
||||
matchCount: matches.length,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RouteSection extends StatelessWidget {
|
||||
final DecodedLogRxRoute route;
|
||||
final List<ResolvedNodeHash> path;
|
||||
final ResolvedNodeHash? originalSender;
|
||||
|
||||
const _RouteSection({
|
||||
required this.route,
|
||||
required this.path,
|
||||
required this.originalSender,
|
||||
});
|
||||
const _RouteSection({required this.route, required this.path});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -820,26 +854,20 @@ class _RouteSection extends StatelessWidget {
|
||||
children: [
|
||||
_FactCard(
|
||||
icon: Icons.route,
|
||||
label: 'Payload',
|
||||
label: AppLocalizations.of(context)!.payload,
|
||||
value: _payloadTypeLabel(route.payloadType),
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.hub,
|
||||
label: 'Hops',
|
||||
label: AppLocalizations.of(context)!.hops,
|
||||
value: '${route.hopCount}',
|
||||
),
|
||||
_FactCard(
|
||||
icon: Icons.tag,
|
||||
label: 'Hash size',
|
||||
label: AppLocalizations.of(context)!.hashSize,
|
||||
value:
|
||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
||||
),
|
||||
if (originalSender != null)
|
||||
_FactCard(
|
||||
icon: Icons.person_pin_circle,
|
||||
label: 'Original sender',
|
||||
value: _nodeLabel(originalSender!),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
251
lib/screens/profiles_screen.dart
Normal file
251
lib/screens/profiles_screen.dart
Normal file
@@ -0,0 +1,251 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../services/profile_manager.dart';
|
||||
import '../services/profile_workspace_coordinator.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class ProfilesScreen extends StatelessWidget {
|
||||
const ProfilesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Consumer<ProfileManager>(
|
||||
builder: (context, profileManager, child) {
|
||||
final profiles = profileManager.visibleProfiles;
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.profiles),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.importProfileFromFile();
|
||||
},
|
||||
icon: const Icon(Icons.file_open),
|
||||
tooltip: 'Import profile',
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: () => _createProfile(context),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(AppLocalizations.of(context)!.newProfile),
|
||||
),
|
||||
body: profiles.isEmpty
|
||||
? Center(
|
||||
child: Text(AppLocalizations.of(context)!.enableProfilesToStartManagingThem),
|
||||
)
|
||||
: ListView.builder(
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
final isActive =
|
||||
profileManager.activeProfileId == profile.id;
|
||||
return Card(
|
||||
margin: const EdgeInsets.only(bottom: 12),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
profile.name,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleMedium,
|
||||
),
|
||||
),
|
||||
if (isActive)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(
|
||||
context,
|
||||
).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(AppLocalizations.of(context)!.active),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(_summary(profile)),
|
||||
const SizedBox(height: 12),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.openProfile(profile.id);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.open),
|
||||
),
|
||||
OutlinedButton(
|
||||
onPressed: () async {
|
||||
final resolved =
|
||||
profile.id ==
|
||||
ConfigProfile.defaultProfileId
|
||||
? await context
|
||||
.read<
|
||||
ProfileWorkspaceCoordinator
|
||||
>()
|
||||
.snapshotCurrentProfile(
|
||||
id: profile.id,
|
||||
name: profile.name,
|
||||
)
|
||||
: profile;
|
||||
if (!context.mounted) return;
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.exportProfile(resolved);
|
||||
},
|
||||
child: Text(AppLocalizations.of(context)!.share),
|
||||
),
|
||||
PopupMenuButton<String>(
|
||||
onSelected: (value) async {
|
||||
switch (value) {
|
||||
case 'duplicate':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.duplicateProfile(profile);
|
||||
break;
|
||||
case 'rename':
|
||||
await _renameProfile(context, profile);
|
||||
break;
|
||||
case 'delete':
|
||||
await context
|
||||
.read<ProfileWorkspaceCoordinator>()
|
||||
.deleteProfile(profile);
|
||||
break;
|
||||
}
|
||||
},
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
value: 'duplicate',
|
||||
child: Text(AppLocalizations.of(context)!.duplicate),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
PopupMenuItem(
|
||||
value: 'rename',
|
||||
child: Text(AppLocalizations.of(context)!.rename),
|
||||
),
|
||||
if (!profile.isDefault)
|
||||
const PopupMenuItem(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
String _summary(ConfigProfile profile) {
|
||||
if (profile.isDefault) {
|
||||
return 'Current app state and history.';
|
||||
}
|
||||
|
||||
final sections = <String>[];
|
||||
if (profile.sections.deviceConfig?.isEmpty == false) {
|
||||
sections.add('Device');
|
||||
}
|
||||
if (profile.sections.channels.isNotEmpty) {
|
||||
sections.add('${profile.sections.channels.length} channels');
|
||||
}
|
||||
if (profile.sections.appSettings?.isEmpty == false) {
|
||||
sections.add('App settings');
|
||||
}
|
||||
if (profile.sections.mapWorkspace?.isEmpty == false) {
|
||||
sections.add('Map workspace');
|
||||
}
|
||||
return sections.isEmpty ? 'Empty profile' : sections.join(' | ');
|
||||
}
|
||||
|
||||
Future<void> _createProfile(BuildContext context) async {
|
||||
final controller = TextEditingController();
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.createProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: Text(AppLocalizations.of(context)!.create),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().createProfileFromCurrent(
|
||||
name: name,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _renameProfile(
|
||||
BuildContext context,
|
||||
ConfigProfile profile,
|
||||
) async {
|
||||
final controller = TextEditingController(text: profile.name);
|
||||
final name = await showDialog<String>(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context)!.renameProfile),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
autofocus: true,
|
||||
decoration: const InputDecoration(labelText: 'Profile name'),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
FilledButton(
|
||||
onPressed: () => Navigator.of(context).pop(controller.text.trim()),
|
||||
child: const Text('Save'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
if (name == null || name.isEmpty || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
await context.read<ProfileWorkspaceCoordinator>().renameProfile(
|
||||
profile,
|
||||
name,
|
||||
);
|
||||
}
|
||||
}
|
||||
776
lib/screens/repeaters_map_screen.dart
Normal file
776
lib/screens/repeaters_map_screen.dart
Normal file
@@ -0,0 +1,776 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart' as flutter_map;
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../providers/contacts_provider.dart';
|
||||
import '../services/mesh_map_nodes_service.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class RepeatersMapScreen extends StatefulWidget {
|
||||
const RepeatersMapScreen({super.key});
|
||||
|
||||
@override
|
||||
State<RepeatersMapScreen> createState() => _RepeatersMapScreenState();
|
||||
}
|
||||
|
||||
class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
||||
static const LatLng _fallbackCenter = LatLng(46.0569, 14.5058);
|
||||
static const double _fallbackZoom = 7;
|
||||
static const double _myLocationZoom = 14;
|
||||
|
||||
final flutter_map.MapController _mapController = flutter_map.MapController();
|
||||
|
||||
List<MeshMapNode> _onlineRepeaters = const [];
|
||||
bool _isLoading = true;
|
||||
bool _didFitCamera = false;
|
||||
String? _error;
|
||||
double _currentZoom = _fallbackZoom;
|
||||
LatLng? _myLocation;
|
||||
final Set<String> _addingRepeaters = <String>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadRepeaters();
|
||||
}
|
||||
|
||||
Future<void> _loadRepeaters({bool forceRefresh = false}) async {
|
||||
setState(() {
|
||||
_isLoading = true;
|
||||
_error = null;
|
||||
if (forceRefresh) {
|
||||
_didFitCamera = false;
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
final nodes = await MeshMapNodesService.fetchNodes(
|
||||
forceRefresh: forceRefresh,
|
||||
);
|
||||
final repeaters = nodes.where(MeshMapNodesService.isRepeater).toList();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_onlineRepeaters = repeaters;
|
||||
_isLoading = false;
|
||||
});
|
||||
_refreshMyLocation();
|
||||
} catch (error) {
|
||||
final cached = await MeshMapNodesService.loadCachedNodes();
|
||||
final repeaters = cached.where(MeshMapNodesService.isRepeater).toList();
|
||||
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_onlineRepeaters = repeaters;
|
||||
_isLoading = false;
|
||||
_error = repeaters.isEmpty
|
||||
? 'Unable to load repeaters right now.'
|
||||
: 'Showing cached repeaters. Pull to retry.';
|
||||
});
|
||||
_refreshMyLocation();
|
||||
}
|
||||
}
|
||||
|
||||
List<_MapRepeater> _mergeRepeaters({
|
||||
required List<MeshMapNode> onlineNodes,
|
||||
required List<Contact> contactRepeaters,
|
||||
}) {
|
||||
final merged = <String, _MapRepeater>{};
|
||||
|
||||
for (final node in onlineNodes) {
|
||||
merged[node.publicKey] = _MapRepeater(
|
||||
publicKey: node.publicKey,
|
||||
name: node.name,
|
||||
latitude: node.latitude,
|
||||
longitude: node.longitude,
|
||||
isFromContacts: false,
|
||||
isFromOnline: true,
|
||||
);
|
||||
}
|
||||
|
||||
for (final contact in contactRepeaters) {
|
||||
final location = contact.displayLocation;
|
||||
if (location == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final key = contact.publicKeyHex.toLowerCase();
|
||||
final existing = merged[key];
|
||||
merged[key] = _MapRepeater(
|
||||
publicKey: key,
|
||||
name: contact.displayName.isNotEmpty
|
||||
? contact.displayName
|
||||
: existing?.name ?? 'Unknown repeater',
|
||||
latitude: existing?.latitude ?? location.latitude,
|
||||
longitude: existing?.longitude ?? location.longitude,
|
||||
isFromContacts: true,
|
||||
isFromOnline: existing?.isFromOnline ?? false,
|
||||
);
|
||||
}
|
||||
|
||||
final repeaters = merged.values.toList()
|
||||
..sort((a, b) => a.name.toLowerCase().compareTo(b.name.toLowerCase()));
|
||||
return repeaters;
|
||||
}
|
||||
|
||||
Future<void> _refreshMyLocation() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: const LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
distanceFilter: 0,
|
||||
),
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_myLocation = LatLng(position.latitude, position.longitude);
|
||||
});
|
||||
} catch (_) {
|
||||
// Best effort only.
|
||||
}
|
||||
}
|
||||
|
||||
void _fitCameraIfNeeded(List<_MapRepeater> repeaters) {
|
||||
if (_didFitCamera || repeaters.isEmpty || !mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!mounted || _didFitCamera || repeaters.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (repeaters.length == 1) {
|
||||
final repeater = repeaters.first;
|
||||
_mapController.move(LatLng(repeater.latitude, repeater.longitude), 13);
|
||||
_currentZoom = 13;
|
||||
} else {
|
||||
final bounds = flutter_map.LatLngBounds.fromPoints(
|
||||
repeaters
|
||||
.map((node) => LatLng(node.latitude, node.longitude))
|
||||
.toList(),
|
||||
);
|
||||
_mapController.fitCamera(
|
||||
flutter_map.CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(48),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_didFitCamera = true;
|
||||
});
|
||||
}
|
||||
|
||||
void _showRepeaterDetails(_MapRepeater repeater) {
|
||||
final isAdding = _addingRepeaters.contains(repeater.publicKey);
|
||||
final canAdd = !repeater.isFromContacts;
|
||||
final isConnected = context
|
||||
.read<ConnectionProvider>()
|
||||
.deviceInfo
|
||||
.isConnected;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
showDragHandle: true,
|
||||
builder: (context) => SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
repeater.name,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
_InfoLine(
|
||||
label: AppLocalizations.of(context)!.publicKeyLabel,
|
||||
value: repeater.publicKey,
|
||||
monospace: true,
|
||||
),
|
||||
_InfoLine(
|
||||
label: AppLocalizations.of(context)!.coordinates,
|
||||
value:
|
||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||
),
|
||||
_InfoLine(label: AppLocalizations.of(context)!.source, value: repeater.sourceLabel),
|
||||
const SizedBox(height: 12),
|
||||
if (canAdd)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: !isConnected || isAdding
|
||||
? null
|
||||
: () async {
|
||||
Navigator.of(context).pop();
|
||||
await _addRepeaterToContacts(repeater);
|
||||
},
|
||||
icon: isAdding
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(Icons.person_add_alt_1),
|
||||
label: Text(
|
||||
!isConnected
|
||||
? 'Connect device to add'
|
||||
: 'Add to contacts',
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: FilledButton.icon(
|
||||
onPressed: null,
|
||||
icon: Icon(Icons.check_circle),
|
||||
label: Text(AppLocalizations.of(context)!.alreadyInContacts),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _addRepeaterToContacts(_MapRepeater repeater) async {
|
||||
if (_addingRepeaters.contains(repeater.publicKey)) {
|
||||
return;
|
||||
}
|
||||
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.connectToADeviceBeforeAddingContacts),
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_addingRepeaters.add(repeater.publicKey);
|
||||
});
|
||||
|
||||
try {
|
||||
await connectionProvider.getContact(_hexToBytes(repeater.publicKey));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${repeater.name} added to contacts')),
|
||||
);
|
||||
} catch (error) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to add ${repeater.name}: $error')),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_addingRepeaters.remove(repeater.publicKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List _hexToBytes(String hex) {
|
||||
final normalized = hex.replaceAll(':', '').trim().toLowerCase();
|
||||
return Uint8List.fromList(
|
||||
List<int>.generate(
|
||||
normalized.length ~/ 2,
|
||||
(index) => int.parse(
|
||||
normalized.substring(index * 2, index * 2 + 2),
|
||||
radix: 16,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactsProvider = context.watch<ContactsProvider>();
|
||||
final repeaters = _mergeRepeaters(
|
||||
onlineNodes: _onlineRepeaters,
|
||||
contactRepeaters: contactsProvider.repeaters,
|
||||
);
|
||||
_fitCameraIfNeeded(repeaters);
|
||||
final clusters = _buildClusters(repeaters);
|
||||
final markers = clusters.map((cluster) {
|
||||
final markerColor = switch (cluster.status) {
|
||||
_ClusterStatus.contactsOnly => const Color(0xFF7C3AED),
|
||||
_ClusterStatus.onlineOnly => const Color(0xFFE8681D),
|
||||
_ClusterStatus.both => const Color(0xFF1B8F4F),
|
||||
_ClusterStatus.mixed => const Color(0xFF2563EB),
|
||||
};
|
||||
|
||||
if (cluster.members.length == 1) {
|
||||
final repeater = cluster.members.first;
|
||||
return flutter_map.Marker(
|
||||
point: cluster.center,
|
||||
width: 80,
|
||||
height: 100,
|
||||
child: GestureDetector(
|
||||
onTap: () => _showRepeaterDetails(repeater),
|
||||
child: _RepeaterMarker(color: markerColor, label: repeater.name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return flutter_map.Marker(
|
||||
point: cluster.center,
|
||||
width: 48,
|
||||
height: 48,
|
||||
child: GestureDetector(
|
||||
onTap: () => _zoomToCluster(cluster),
|
||||
child: _ClusterMarker(
|
||||
color: markerColor,
|
||||
count: cluster.members.length,
|
||||
),
|
||||
),
|
||||
);
|
||||
}).toList();
|
||||
final allMarkers = <flutter_map.Marker>[
|
||||
...markers,
|
||||
if (_myLocation != null)
|
||||
flutter_map.Marker(
|
||||
point: _myLocation!,
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFF2563EB),
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 3),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.18),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: Text(AppLocalizations.of(context)!.repeatersMap),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: _isLoading
|
||||
? null
|
||||
: () => _loadRepeaters(forceRefresh: true),
|
||||
icon: const Icon(Icons.refresh),
|
||||
tooltip: 'Refresh',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Stack(
|
||||
children: [
|
||||
flutter_map.FlutterMap(
|
||||
mapController: _mapController,
|
||||
options: flutter_map.MapOptions(
|
||||
initialCenter: _fallbackCenter,
|
||||
initialZoom: _fallbackZoom,
|
||||
onPositionChanged: (camera, hasGesture) {
|
||||
final nextZoom = camera.zoom;
|
||||
if (nextZoom != _currentZoom && mounted) {
|
||||
setState(() {
|
||||
_currentZoom = nextZoom;
|
||||
});
|
||||
}
|
||||
},
|
||||
),
|
||||
children: [
|
||||
flutter_map.TileLayer(
|
||||
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
userAgentPackageName: 'app.meshcore.sar',
|
||||
),
|
||||
flutter_map.MarkerLayer(markers: allMarkers),
|
||||
],
|
||||
),
|
||||
if (_error != null)
|
||||
Positioned(
|
||||
top: 16,
|
||||
left: 16,
|
||||
right: 16,
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(_error!),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (_isLoading) const Center(child: CircularProgressIndicator()),
|
||||
if (!_isLoading && repeaters.isEmpty)
|
||||
Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Text(
|
||||
_error ?? 'No repeaters available.',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (repeaters.isNotEmpty)
|
||||
Positioned(
|
||||
left: 16,
|
||||
top: _error == null ? 16 : 72,
|
||||
child: Material(
|
||||
elevation: 2,
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 14,
|
||||
vertical: 10,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_LegendRow(
|
||||
color: const Color(0xFF7C3AED),
|
||||
label: AppLocalizations.of(context)!.fromContacts,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(
|
||||
color: const Color(0xFFE8681D),
|
||||
label: AppLocalizations.of(context)!.onlineOnly,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_LegendRow(color: const Color(0xFF1B8F4F), label: AppLocalizations.of(context)!.inBoth),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (repeaters.isNotEmpty)
|
||||
Positioned(
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
child: Material(
|
||||
elevation: 3,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: Text(
|
||||
'${repeaters.length} repeaters',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
floatingActionButton: FloatingActionButton(
|
||||
onPressed: _jumpToMyLocation,
|
||||
tooltip: 'My location',
|
||||
child: const Icon(Icons.my_location),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _jumpToMyLocation() async {
|
||||
await _refreshMyLocation();
|
||||
final myLocation = _myLocation;
|
||||
if (!mounted || myLocation == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
_mapController.move(myLocation, _myLocationZoom);
|
||||
setState(() {
|
||||
_currentZoom = _myLocationZoom;
|
||||
});
|
||||
}
|
||||
|
||||
List<_RepeaterCluster> _buildClusters(List<_MapRepeater> repeaters) {
|
||||
if (repeaters.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
if (_currentZoom >= 11.5) {
|
||||
return repeaters
|
||||
.map(
|
||||
(repeater) => _RepeaterCluster(
|
||||
members: [repeater],
|
||||
center: LatLng(repeater.latitude, repeater.longitude),
|
||||
status: repeater.clusterStatus,
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
final cellSize = _gridSizeForZoom(_currentZoom);
|
||||
final buckets = <String, List<_MapRepeater>>{};
|
||||
|
||||
for (final repeater in repeaters) {
|
||||
final latBucket = (repeater.latitude / cellSize).floor();
|
||||
final lonBucket = (repeater.longitude / cellSize).floor();
|
||||
final key = '$latBucket:$lonBucket';
|
||||
buckets.putIfAbsent(key, () => <_MapRepeater>[]).add(repeater);
|
||||
}
|
||||
|
||||
return buckets.values.map((members) {
|
||||
final centerLat =
|
||||
members.map((node) => node.latitude).reduce((a, b) => a + b) /
|
||||
members.length;
|
||||
final centerLon =
|
||||
members.map((node) => node.longitude).reduce((a, b) => a + b) /
|
||||
members.length;
|
||||
final statuses = members.map((node) => node.clusterStatus).toSet();
|
||||
return _RepeaterCluster(
|
||||
members: members,
|
||||
center: LatLng(centerLat, centerLon),
|
||||
status: statuses.length == 1 ? statuses.first : _ClusterStatus.mixed,
|
||||
);
|
||||
}).toList();
|
||||
}
|
||||
|
||||
double _gridSizeForZoom(double zoom) {
|
||||
if (zoom < 5) return 3.0;
|
||||
if (zoom < 7) return 1.5;
|
||||
if (zoom < 9) return 0.7;
|
||||
if (zoom < 10.5) return 0.28;
|
||||
return 0.12;
|
||||
}
|
||||
|
||||
void _zoomToCluster(_RepeaterCluster cluster) {
|
||||
if (cluster.members.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final bounds = flutter_map.LatLngBounds.fromPoints(
|
||||
cluster.members
|
||||
.map((node) => LatLng(node.latitude, node.longitude))
|
||||
.toList(),
|
||||
);
|
||||
_mapController.fitCamera(
|
||||
flutter_map.CameraFit.bounds(
|
||||
bounds: bounds,
|
||||
padding: const EdgeInsets.all(64),
|
||||
maxZoom: (_currentZoom + 2).clamp(8, 14),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _InfoLine extends StatelessWidget {
|
||||
final String label;
|
||||
final String value;
|
||||
final bool monospace;
|
||||
|
||||
const _InfoLine({
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.monospace = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 92,
|
||||
child: Text(
|
||||
label,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value,
|
||||
style: monospace
|
||||
? const TextStyle(fontFamily: 'monospace')
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LegendRow extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
const _LegendRow({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(color: color, shape: BoxShape.circle),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(label),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RepeaterMarker extends StatelessWidget {
|
||||
final Color color;
|
||||
final String label;
|
||||
|
||||
const _RepeaterMarker({required this.color, required this.label});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: const Icon(Icons.router, color: Colors.white, size: 18),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ClusterMarker extends StatelessWidget {
|
||||
final Color color;
|
||||
final int count;
|
||||
|
||||
const _ClusterMarker({required this.color, required this.count});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
border: Border.all(color: Colors.white, width: 2),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.22),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
],
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
'$count',
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.w800,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _ClusterStatus { contactsOnly, onlineOnly, both, mixed }
|
||||
|
||||
class _RepeaterCluster {
|
||||
final List<_MapRepeater> members;
|
||||
final LatLng center;
|
||||
final _ClusterStatus status;
|
||||
|
||||
const _RepeaterCluster({
|
||||
required this.members,
|
||||
required this.center,
|
||||
required this.status,
|
||||
});
|
||||
}
|
||||
|
||||
class _MapRepeater {
|
||||
final String publicKey;
|
||||
final String name;
|
||||
final double latitude;
|
||||
final double longitude;
|
||||
final bool isFromContacts;
|
||||
final bool isFromOnline;
|
||||
|
||||
const _MapRepeater({
|
||||
required this.publicKey,
|
||||
required this.name,
|
||||
required this.latitude,
|
||||
required this.longitude,
|
||||
required this.isFromContacts,
|
||||
required this.isFromOnline,
|
||||
});
|
||||
|
||||
String get sourceLabel {
|
||||
if (isFromContacts && isFromOnline) return 'Contacts and online';
|
||||
if (isFromContacts) return 'Contacts';
|
||||
return 'Online';
|
||||
}
|
||||
|
||||
_ClusterStatus get clusterStatus {
|
||||
if (isFromContacts && isFromOnline) return _ClusterStatus.both;
|
||||
if (isFromContacts) return _ClusterStatus.contactsOnly;
|
||||
return _ClusterStatus.onlineOnly;
|
||||
}
|
||||
}
|
||||
@@ -240,7 +240,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'import',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.download),
|
||||
leading: Icon(Icons.download),
|
||||
title: Text(l10n.importFromClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -248,7 +248,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'export',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.upload),
|
||||
leading: Icon(Icons.upload),
|
||||
title: Text(l10n.exportToClipboard),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -257,7 +257,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
PopupMenuItem(
|
||||
value: 'reset',
|
||||
child: ListTile(
|
||||
leading: const Icon(Icons.restart_alt),
|
||||
leading: Icon(Icons.restart_alt),
|
||||
title: Text(l10n.resetToDefaults),
|
||||
contentPadding: EdgeInsets.zero,
|
||||
),
|
||||
@@ -283,14 +283,14 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
size: 64,
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(height: 16),
|
||||
Text(
|
||||
l10n.noTemplates,
|
||||
style: theme.textTheme.titleMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
l10n.tapAddToCreate,
|
||||
style: theme.textTheme.bodyMedium?.copyWith(
|
||||
@@ -317,7 +317,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
||||
),
|
||||
floatingActionButton: FloatingActionButton.extended(
|
||||
onPressed: _addTemplate,
|
||||
icon: const Icon(Icons.add),
|
||||
icon: Icon(Icons.add),
|
||||
label: Text(l10n.addTemplate),
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ import 'package:provider/provider.dart';
|
||||
import '../models/device_info.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../widgets/device/spectrum_scan_panel.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
|
||||
class SpectrumScanScreen extends StatefulWidget {
|
||||
const SpectrumScanScreen({super.key});
|
||||
@@ -323,8 +324,8 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
||||
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
||||
if (mergedCandidates.isEmpty) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Spectrum scan returned no candidate frequencies'),
|
||||
SnackBar(
|
||||
content: Text(AppLocalizations.of(context)!.spectrumScanReturnedNoCandidateFrequencies),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
@@ -383,7 +384,7 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
||||
: possibleBracketFrequencies;
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Spectrum Scan')),
|
||||
appBar: AppBar(title: Text(AppLocalizations.of(context)!.spectrumScan)),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
import '../services/wizard_preferences.dart';
|
||||
import '../widgets/connection_dialog.dart';
|
||||
|
||||
/// Welcome wizard screen to introduce new users to the app
|
||||
class WelcomeWizardScreen extends StatefulWidget {
|
||||
@@ -14,12 +17,171 @@ class WelcomeWizardScreen extends StatefulWidget {
|
||||
|
||||
class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
final PageController _pageController = PageController();
|
||||
final TextEditingController _deviceNameController = TextEditingController();
|
||||
int _currentPage = 0;
|
||||
static const int _totalPages = 5;
|
||||
bool _isApplyingDeviceSetup = false;
|
||||
static const int _totalPages = 6;
|
||||
static const List<_RadioPreset> _radioPresets = [
|
||||
// Official MeshCore config presets from https://api.meshcore.nz/api/v1/config
|
||||
_RadioPreset(
|
||||
id: 'australia',
|
||||
label: 'Australia',
|
||||
summary: '915.800 MHz, BW 250 kHz, SF10, CR5',
|
||||
frequencyKhz: 915800,
|
||||
bandwidth: 8,
|
||||
spreadingFactor: 10,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'australia_narrow',
|
||||
label: 'Australia (Narrow)',
|
||||
summary: '916.575 MHz, BW 62.5 kHz, SF7, CR8',
|
||||
frequencyKhz: 916575,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 7,
|
||||
codingRate: 8,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'australia_sa_wa',
|
||||
label: 'Australia: SA, WA',
|
||||
summary: '923.125 MHz, BW 62.5 kHz, SF8, CR8',
|
||||
frequencyKhz: 923125,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 8,
|
||||
codingRate: 8,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'australia_qld',
|
||||
label: 'Australia: QLD',
|
||||
summary: '923.125 MHz, BW 62.5 kHz, SF8, CR5',
|
||||
frequencyKhz: 923125,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 8,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'eu_uk_narrow',
|
||||
label: 'EU/UK (Narrow)',
|
||||
summary: '869.618 MHz, BW 62.5 kHz, SF8, CR8',
|
||||
frequencyKhz: 869618,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 8,
|
||||
codingRate: 8,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'eu_uk_deprecated',
|
||||
label: 'EU/UK (Deprecated)',
|
||||
summary: '869.525 MHz, BW 250 kHz, SF11, CR5',
|
||||
frequencyKhz: 869525,
|
||||
bandwidth: 8,
|
||||
spreadingFactor: 11,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'czech_republic_narrow',
|
||||
label: 'Czech Republic (Narrow)',
|
||||
summary: '869.432 MHz, BW 62.5 kHz, SF7, CR5',
|
||||
frequencyKhz: 869432,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 7,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'eu_433_long_range',
|
||||
label: 'EU 433MHz (Long Range)',
|
||||
summary: '433.650 MHz, BW 250 kHz, SF11, CR5',
|
||||
frequencyKhz: 433650,
|
||||
bandwidth: 8,
|
||||
spreadingFactor: 11,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'new_zealand',
|
||||
label: 'New Zealand',
|
||||
summary: '917.375 MHz, BW 250 kHz, SF11, CR5',
|
||||
frequencyKhz: 917375,
|
||||
bandwidth: 8,
|
||||
spreadingFactor: 11,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'new_zealand_narrow',
|
||||
label: 'New Zealand (Narrow)',
|
||||
summary: '917.375 MHz, BW 62.5 kHz, SF7, CR5',
|
||||
frequencyKhz: 917375,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 7,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'portugal_433',
|
||||
label: 'Portugal 433',
|
||||
summary: '433.375 MHz, BW 62.5 kHz, SF9, CR6',
|
||||
frequencyKhz: 433375,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 9,
|
||||
codingRate: 6,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'portugal_868',
|
||||
label: 'Portugal 868',
|
||||
summary: '869.618 MHz, BW 62.5 kHz, SF7, CR6',
|
||||
frequencyKhz: 869618,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 7,
|
||||
codingRate: 6,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'switzerland',
|
||||
label: 'Switzerland',
|
||||
summary: '869.618 MHz, BW 62.5 kHz, SF8, CR8',
|
||||
frequencyKhz: 869618,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 8,
|
||||
codingRate: 8,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'usa_canada_recommended',
|
||||
label: 'USA/Canada (Recommended)',
|
||||
summary: '910.525 MHz, BW 62.5 kHz, SF7, CR5',
|
||||
frequencyKhz: 910525,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 7,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'vietnam_narrow',
|
||||
label: 'Vietnam (Narrow)',
|
||||
summary: '920.250 MHz, BW 62.5 kHz, SF8, CR5',
|
||||
frequencyKhz: 920250,
|
||||
bandwidth: 6,
|
||||
spreadingFactor: 8,
|
||||
codingRate: 5,
|
||||
),
|
||||
_RadioPreset(
|
||||
id: 'vietnam_deprecated',
|
||||
label: 'Vietnam (Deprecated)',
|
||||
summary: '920.250 MHz, BW 250 kHz, SF11, CR5',
|
||||
frequencyKhz: 920250,
|
||||
bandwidth: 8,
|
||||
spreadingFactor: 11,
|
||||
codingRate: 5,
|
||||
),
|
||||
];
|
||||
_RadioPreset _selectedPreset = _radioPresets[4];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final deviceInfo = context.read<ConnectionProvider>().deviceInfo;
|
||||
_deviceNameController.text =
|
||||
deviceInfo.selfName ?? deviceInfo.deviceName ?? '';
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_pageController.dispose();
|
||||
_deviceNameController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -30,7 +192,9 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
}
|
||||
|
||||
void _nextPage() {
|
||||
if (_currentPage < _totalPages - 1) {
|
||||
if (_currentPage == 1) {
|
||||
_handleDeviceSetupAction();
|
||||
} else if (_currentPage < _totalPages - 1) {
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
@@ -56,6 +220,98 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _openConnectionDialog() async {
|
||||
await showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => const ConnectionDialog(),
|
||||
);
|
||||
if (!mounted) return;
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
if (connectionProvider.deviceInfo.isConnected) {
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
}
|
||||
if (!mounted) return;
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final fetchedName = deviceInfo.selfName ?? deviceInfo.deviceName ?? '';
|
||||
if (fetchedName.isNotEmpty) {
|
||||
setState(() {
|
||||
_deviceNameController.text = fetchedName;
|
||||
});
|
||||
} else {
|
||||
setState(() {});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _handleDeviceSetupAction() async {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
final connectionProvider = context.read<ConnectionProvider>();
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
if (!deviceInfo.isConnected) {
|
||||
await _openConnectionDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
final trimmedName = _deviceNameController.text.trim();
|
||||
if (trimmedName.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(SnackBar(content: Text(l10n.wizardEnterDeviceName)));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_isApplyingDeviceSetup = true;
|
||||
});
|
||||
|
||||
try {
|
||||
await connectionProvider.setAdvertName(trimmedName);
|
||||
await connectionProvider.setRadioParams(
|
||||
frequency: _selectedPreset.frequencyKhz,
|
||||
bandwidth: _selectedPreset.bandwidth,
|
||||
spreadingFactor: _selectedPreset.spreadingFactor,
|
||||
codingRate: _selectedPreset.codingRate,
|
||||
);
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
l10n.wizardDeviceSetupSaved(trimmedName, _selectedPreset.label),
|
||||
),
|
||||
backgroundColor: Colors.green,
|
||||
),
|
||||
);
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(l10n.failedToSave(e.toString())),
|
||||
backgroundColor: Colors.red,
|
||||
),
|
||||
);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_isApplyingDeviceSetup = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _skipDeviceSetupStep() {
|
||||
if (_isApplyingDeviceSetup) return;
|
||||
_pageController.nextPage(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
@@ -68,14 +324,14 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
children: [
|
||||
// Top bar with skip button
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
if (_currentPage > 0)
|
||||
TextButton.icon(
|
||||
onPressed: _previousPage,
|
||||
icon: const Icon(Icons.arrow_back),
|
||||
icon: Icon(Icons.arrow_back),
|
||||
label: Text(l10n.wizardBack),
|
||||
)
|
||||
else
|
||||
@@ -96,8 +352,10 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
child: PageView(
|
||||
controller: _pageController,
|
||||
onPageChanged: _onPageChanged,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildWelcomePage(context, l10n, colorScheme),
|
||||
_buildConnectDevicePage(context, l10n, colorScheme),
|
||||
_buildConnectingPage(context, l10n, colorScheme),
|
||||
_buildChannelPage(context, l10n, colorScheme),
|
||||
_buildContactsPage(context, l10n, colorScheme),
|
||||
@@ -108,7 +366,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
|
||||
// Page indicators
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 8),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: List.generate(
|
||||
@@ -130,11 +388,11 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
|
||||
// Next/Get Started button
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton(
|
||||
onPressed: _nextPage,
|
||||
onPressed: _isApplyingDeviceSetup ? null : _nextPage,
|
||||
style: ElevatedButton.styleFrom(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16.0),
|
||||
shape: RoundedRectangleBorder(
|
||||
@@ -142,7 +400,9 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
_currentPage < _totalPages - 1
|
||||
_currentPage == 1
|
||||
? _deviceSetupButtonLabel(context)
|
||||
: _currentPage < _totalPages - 1
|
||||
? l10n.wizardNext
|
||||
: l10n.wizardGetStarted,
|
||||
style: const TextStyle(fontSize: 16),
|
||||
@@ -165,7 +425,21 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
icon: Icons.waving_hand,
|
||||
iconColor: Colors.orange,
|
||||
title: l10n.wizardWelcomeTitle,
|
||||
description: l10n.wizardWelcomeDescription,
|
||||
description: l10n.wizardOverviewDescription,
|
||||
features: [
|
||||
_FeatureItem(
|
||||
icon: Icons.chat_bubble_outline,
|
||||
text: l10n.wizardOverviewFeature1,
|
||||
),
|
||||
_FeatureItem(
|
||||
icon: Icons.emergency_share,
|
||||
text: l10n.wizardOverviewFeature2,
|
||||
),
|
||||
_FeatureItem(
|
||||
icon: Icons.settings_input_antenna,
|
||||
text: l10n.wizardOverviewFeature3,
|
||||
),
|
||||
],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
}
|
||||
@@ -176,33 +450,225 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return _buildPage(
|
||||
icon: Icons.bluetooth_searching,
|
||||
icon: Icons.forum_rounded,
|
||||
iconColor: Colors.blue,
|
||||
title: l10n.wizardConnectingTitle,
|
||||
description: l10n.wizardConnectingDescription,
|
||||
title: l10n.wizardMessagingTitle,
|
||||
description: l10n.wizardMessagingDescription,
|
||||
features: [
|
||||
_FeatureItem(icon: Icons.radio, text: l10n.wizardConnectingFeature1),
|
||||
_FeatureItem(icon: Icons.link, text: l10n.wizardConnectingFeature2),
|
||||
_FeatureItem(icon: Icons.wifi_off, text: l10n.wizardConnectingFeature3),
|
||||
_FeatureItem(icon: Icons.chat, text: l10n.wizardMessagingFeature1),
|
||||
_FeatureItem(icon: Icons.campaign, text: l10n.wizardMessagingFeature2),
|
||||
_FeatureItem(icon: Icons.mic, text: l10n.wizardMessagingFeature3),
|
||||
],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildConnectDevicePage(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
final connectionProvider = context.watch<ConnectionProvider>();
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
final isConnected = deviceInfo.isConnected;
|
||||
final connectedName = deviceInfo.selfName ?? deviceInfo.deviceName;
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 600,
|
||||
maxHeight: constraints.maxHeight,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_buildPageHeader(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: Icons.bluetooth_connected,
|
||||
iconColor: Colors.green,
|
||||
title: l10n.wizardConnectDeviceTitle,
|
||||
description: l10n.wizardConnectDeviceDescription,
|
||||
badge: l10n.wizardSetupBadge,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContentCard(
|
||||
colorScheme: colorScheme,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
isConnected
|
||||
? Icons.check_circle
|
||||
: Icons.bluetooth_searching,
|
||||
color: isConnected
|
||||
? colorScheme.primary
|
||||
: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
isConnected
|
||||
? l10n.wizardConnectedToDevice(
|
||||
connectedName ?? l10n.device,
|
||||
)
|
||||
: l10n.wizardNoDeviceConnected,
|
||||
style: Theme.of(context).textTheme.bodyMedium
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: _openConnectionDialog,
|
||||
child: Text(
|
||||
isConnected ? l10n.change : l10n.connect,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: TextButton(
|
||||
onPressed: _skipDeviceSetupStep,
|
||||
child: Text(l10n.wizardSkipForNow),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildContentCard(
|
||||
colorScheme: colorScheme,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextField(
|
||||
controller: _deviceNameController,
|
||||
textInputAction: TextInputAction.done,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.wizardDeviceNameLabel,
|
||||
border: OutlineInputBorder(),
|
||||
helperText: l10n.wizardDeviceNameHelp,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
DropdownButtonFormField<_RadioPreset>(
|
||||
initialValue: _selectedPreset,
|
||||
isExpanded: true,
|
||||
decoration: InputDecoration(
|
||||
labelText: l10n.wizardConfigRegionLabel,
|
||||
border: OutlineInputBorder(),
|
||||
helperText: l10n.wizardConfigRegionHelp,
|
||||
),
|
||||
items: _radioPresets
|
||||
.map(
|
||||
(preset) => DropdownMenuItem<_RadioPreset>(
|
||||
value: preset,
|
||||
child: Text(
|
||||
preset.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (preset) {
|
||||
if (preset == null) return;
|
||||
setState(() {
|
||||
_selectedPreset = preset;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildContentCard(
|
||||
colorScheme: colorScheme,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
_selectedPreset.label,
|
||||
style: Theme.of(context).textTheme.titleMedium
|
||||
?.copyWith(fontWeight: FontWeight.w700),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_selectedPreset.summary,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
SizedBox(height: 14),
|
||||
...[
|
||||
_FeatureItem(
|
||||
icon: Icons.rule,
|
||||
text: l10n.wizardPresetNote1,
|
||||
),
|
||||
_FeatureItem(
|
||||
icon: Icons.settings_input_antenna,
|
||||
text: l10n.wizardPresetNote2,
|
||||
),
|
||||
_FeatureItem(
|
||||
icon: Icons.public,
|
||||
text: l10n.wizardPresetNote3,
|
||||
),
|
||||
].map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||||
child: _buildFeatureRow(
|
||||
context,
|
||||
colorScheme,
|
||||
feature,
|
||||
iconSize: 20,
|
||||
gap: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _deviceSetupButtonLabel(BuildContext context) {
|
||||
final isConnected = context
|
||||
.watch<ConnectionProvider>()
|
||||
.deviceInfo
|
||||
.isConnected;
|
||||
if (_isApplyingDeviceSetup) {
|
||||
return AppLocalizations.of(context)!.wizardSaving;
|
||||
}
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return isConnected
|
||||
? l10n.wizardSaveAndContinue
|
||||
: l10n.wizardConnectDeviceTitle;
|
||||
}
|
||||
|
||||
Widget _buildChannelPage(
|
||||
BuildContext context,
|
||||
AppLocalizations l10n,
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return _buildPage(
|
||||
icon: Icons.campaign,
|
||||
icon: Icons.groups_rounded,
|
||||
iconColor: Colors.purple,
|
||||
title: l10n.wizardChannelTitle,
|
||||
description: l10n.wizardChannelDescription,
|
||||
title: l10n.wizardNetworkTitle,
|
||||
description: l10n.wizardNetworkDescription,
|
||||
features: [
|
||||
_FeatureItem(icon: Icons.public, text: l10n.wizardChannelFeature1),
|
||||
_FeatureItem(icon: Icons.groups, text: l10n.wizardChannelFeature2),
|
||||
_FeatureItem(icon: Icons.send, text: l10n.wizardChannelFeature3),
|
||||
_FeatureItem(
|
||||
icon: Icons.person_add_alt_1,
|
||||
text: l10n.wizardNetworkFeature1,
|
||||
),
|
||||
_FeatureItem(icon: Icons.route, text: l10n.wizardNetworkFeature2),
|
||||
_FeatureItem(icon: Icons.hub, text: l10n.wizardNetworkFeature3),
|
||||
],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
@@ -214,17 +680,14 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return _buildPage(
|
||||
icon: Icons.people,
|
||||
icon: Icons.map_rounded,
|
||||
iconColor: Colors.teal,
|
||||
title: l10n.wizardContactsTitle,
|
||||
description: l10n.wizardContactsDescription,
|
||||
title: l10n.wizardMapOpsTitle,
|
||||
description: l10n.wizardMapOpsDescription,
|
||||
features: [
|
||||
_FeatureItem(icon: Icons.person_add, text: l10n.wizardContactsFeature1),
|
||||
_FeatureItem(icon: Icons.chat, text: l10n.wizardContactsFeature2),
|
||||
_FeatureItem(
|
||||
icon: Icons.battery_std,
|
||||
text: l10n.wizardContactsFeature3,
|
||||
),
|
||||
_FeatureItem(icon: Icons.location_on, text: l10n.wizardMapOpsFeature1),
|
||||
_FeatureItem(icon: Icons.draw, text: l10n.wizardMapOpsFeature2),
|
||||
_FeatureItem(icon: Icons.router, text: l10n.wizardMapOpsFeature3),
|
||||
],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
@@ -236,18 +699,17 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
ColorScheme colorScheme,
|
||||
) {
|
||||
return _buildPage(
|
||||
icon: Icons.map,
|
||||
icon: Icons.tune_rounded,
|
||||
iconColor: Colors.red,
|
||||
title: l10n.wizardMapTitle,
|
||||
description: l10n.wizardMapDescription,
|
||||
title: l10n.wizardToolsTitle,
|
||||
description: l10n.wizardToolsDescription,
|
||||
features: [
|
||||
_FeatureItem(icon: Icons.location_on, text: l10n.wizardMapFeature1),
|
||||
_FeatureItem(icon: Icons.settings, text: l10n.wizardToolsFeature1),
|
||||
_FeatureItem(icon: Icons.sensors, text: l10n.wizardToolsFeature2),
|
||||
_FeatureItem(
|
||||
icon: Icons.person_pin_circle,
|
||||
text: l10n.wizardMapFeature2,
|
||||
icon: Icons.analytics_outlined,
|
||||
text: l10n.wizardToolsFeature3,
|
||||
),
|
||||
_FeatureItem(icon: Icons.offline_pin, text: l10n.wizardMapFeature3),
|
||||
_FeatureItem(icon: Icons.draw, text: l10n.wizardMapFeature4),
|
||||
],
|
||||
colorScheme: colorScheme,
|
||||
);
|
||||
@@ -261,69 +723,149 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
||||
List<_FeatureItem>? features,
|
||||
required ColorScheme colorScheme,
|
||||
}) {
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const SizedBox(height: 20),
|
||||
// Icon
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 80, color: iconColor),
|
||||
final l10n = AppLocalizations.of(context)!;
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) => Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: 560,
|
||||
maxHeight: constraints.maxHeight,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// Title
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// Description
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.7),
|
||||
height: 1.5,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (features != null && features.isNotEmpty) ...[
|
||||
const SizedBox(height: 32),
|
||||
// Features list
|
||||
...features.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(feature.icon, color: colorScheme.primary, size: 24),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Text(
|
||||
feature.text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 12, 20, 12),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_buildPageHeader(
|
||||
context: context,
|
||||
colorScheme: colorScheme,
|
||||
icon: icon,
|
||||
iconColor: iconColor,
|
||||
title: title,
|
||||
description: description,
|
||||
badge: l10n.wizardOverviewBadge,
|
||||
),
|
||||
),
|
||||
if (features != null && features.isNotEmpty) ...[
|
||||
const SizedBox(height: 18),
|
||||
_buildContentCard(
|
||||
colorScheme: colorScheme,
|
||||
child: Column(
|
||||
children: features
|
||||
.map(
|
||||
(feature) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
||||
child: _buildFeatureRow(
|
||||
context,
|
||||
colorScheme,
|
||||
feature,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPageHeader({
|
||||
required BuildContext context,
|
||||
required ColorScheme colorScheme,
|
||||
required IconData icon,
|
||||
required Color iconColor,
|
||||
required String title,
|
||||
required String description,
|
||||
required String badge,
|
||||
}) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
),
|
||||
child: Text(
|
||||
badge,
|
||||
style: Theme.of(context).textTheme.labelLarge?.copyWith(
|
||||
color: colorScheme.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: iconColor.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 56, color: iconColor),
|
||||
),
|
||||
const SizedBox(height: 18),
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: colorScheme.onSurface.withValues(alpha: 0.72),
|
||||
height: 1.4,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContentCard({
|
||||
required ColorScheme colorScheme,
|
||||
required Widget child,
|
||||
}) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFeatureRow(
|
||||
BuildContext context,
|
||||
ColorScheme colorScheme,
|
||||
_FeatureItem feature, {
|
||||
double iconSize = 22,
|
||||
double gap = 14,
|
||||
}) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(feature.icon, color: colorScheme.primary, size: iconSize),
|
||||
SizedBox(width: gap),
|
||||
Expanded(
|
||||
child: Text(
|
||||
feature.text,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyMedium?.copyWith(color: colorScheme.onSurface),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeatureItem {
|
||||
@@ -332,3 +874,23 @@ class _FeatureItem {
|
||||
|
||||
_FeatureItem({required this.icon, required this.text});
|
||||
}
|
||||
|
||||
class _RadioPreset {
|
||||
final String id;
|
||||
final String label;
|
||||
final String summary;
|
||||
final int frequencyKhz;
|
||||
final int bandwidth;
|
||||
final int spreadingFactor;
|
||||
final int codingRate;
|
||||
|
||||
const _RadioPreset({
|
||||
required this.id,
|
||||
required this.label,
|
||||
required this.summary,
|
||||
required this.frequencyKhz,
|
||||
required this.bandwidth,
|
||||
required this.spreadingFactor,
|
||||
required this.codingRate,
|
||||
});
|
||||
}
|
||||
|
||||
156
lib/services/app_config_snapshot_service.dart
Normal file
156
lib/services/app_config_snapshot_service.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/app_provider.dart';
|
||||
import 'image_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'route_hash_preferences.dart';
|
||||
import 'voice_bitrate_preferences.dart';
|
||||
|
||||
class AppConfigSnapshotService {
|
||||
Future<AppSettingsProfileSection> capture(AppProvider appProvider) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
return AppSettingsProfileSection(
|
||||
mapEnabled: appProvider.isMapEnabled,
|
||||
contactsEnabled: appProvider.isContactsEnabled,
|
||||
sensorsEnabled: appProvider.isSensorsEnabled,
|
||||
voiceSilenceTrimmingEnabled: appProvider.isVoiceSilenceTrimmingEnabled,
|
||||
voiceBandPassFilterEnabled: appProvider.isVoiceBandPassFilterEnabled,
|
||||
voiceCompressorEnabled: appProvider.isVoiceCompressorEnabled,
|
||||
voiceLimiterEnabled: appProvider.isVoiceLimiterEnabled,
|
||||
voiceAutoGainEnabled: appProvider.isVoiceAutoGainEnabled,
|
||||
voiceEchoCancellationEnabled: appProvider.isVoiceEchoCancellationEnabled,
|
||||
voiceNoiseSuppressionEnabled: appProvider.isVoiceNoiseSuppressionEnabled,
|
||||
messageFontScale: appProvider.messageFontScale,
|
||||
autoRouteRotationEnabled: appProvider.autoRouteRotationEnabled,
|
||||
clearPathOnMaxRetry: appProvider.clearPathOnMaxRetry,
|
||||
nearestRelayFallbackEnabled: appProvider.nearestRelayFallbackEnabled,
|
||||
voiceBitrate: await VoiceBitratePreferences.getBitrate(),
|
||||
routeHashSize: await RouteHashPreferences.getHashSize(),
|
||||
imageMaxSize: await ImagePreferences.getMaxSize(),
|
||||
imageCompression: await ImagePreferences.getCompression(),
|
||||
imageGrayscale: await ImagePreferences.getGrayscale(),
|
||||
imageUltraMode: await ImagePreferences.getUltraMode(),
|
||||
showRxTxIndicators:
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
) ??
|
||||
true,
|
||||
fastLocationUpdatesEnabled: locationTracking.fastLocationUpdatesEnabled,
|
||||
fastLocationMovementThresholdMeters:
|
||||
locationTracking.fastLocationMovementThresholdMeters,
|
||||
fastLocationActiveCadenceSeconds:
|
||||
locationTracking.fastLocationActiveCadenceSeconds,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
AppSettingsProfileSection? section,
|
||||
AppProvider appProvider,
|
||||
) async {
|
||||
if (section == null || section.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (section.mapEnabled != null) {
|
||||
await appProvider.toggleMapEnabled(section.mapEnabled!);
|
||||
}
|
||||
if (section.contactsEnabled != null) {
|
||||
await appProvider.toggleContactsEnabled(section.contactsEnabled!);
|
||||
}
|
||||
if (section.sensorsEnabled != null) {
|
||||
await appProvider.toggleSensorsEnabled(section.sensorsEnabled!);
|
||||
}
|
||||
if (section.voiceSilenceTrimmingEnabled != null) {
|
||||
await appProvider.toggleVoiceSilenceTrimmingEnabled(
|
||||
section.voiceSilenceTrimmingEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBandPassFilterEnabled != null) {
|
||||
await appProvider.toggleVoiceBandPassFilterEnabled(
|
||||
section.voiceBandPassFilterEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceCompressorEnabled != null) {
|
||||
await appProvider.toggleVoiceCompressorEnabled(
|
||||
section.voiceCompressorEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceLimiterEnabled != null) {
|
||||
await appProvider.toggleVoiceLimiterEnabled(section.voiceLimiterEnabled!);
|
||||
}
|
||||
if (section.voiceAutoGainEnabled != null) {
|
||||
await appProvider.toggleVoiceAutoGainEnabled(
|
||||
section.voiceAutoGainEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceEchoCancellationEnabled != null) {
|
||||
await appProvider.toggleVoiceEchoCancellationEnabled(
|
||||
section.voiceEchoCancellationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceNoiseSuppressionEnabled != null) {
|
||||
await appProvider.toggleVoiceNoiseSuppressionEnabled(
|
||||
section.voiceNoiseSuppressionEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.messageFontScale != null) {
|
||||
await appProvider.setMessageFontScale(section.messageFontScale!);
|
||||
}
|
||||
if (section.autoRouteRotationEnabled != null) {
|
||||
await appProvider.toggleAutoRouteRotationEnabled(
|
||||
section.autoRouteRotationEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.clearPathOnMaxRetry != null) {
|
||||
await appProvider.toggleClearPathOnMaxRetry(section.clearPathOnMaxRetry!);
|
||||
}
|
||||
if (section.nearestRelayFallbackEnabled != null) {
|
||||
await appProvider.toggleNearestRelayFallbackEnabled(
|
||||
section.nearestRelayFallbackEnabled!,
|
||||
);
|
||||
}
|
||||
if (section.voiceBitrate != null) {
|
||||
await VoiceBitratePreferences.setBitrate(section.voiceBitrate!);
|
||||
}
|
||||
if (section.routeHashSize != null) {
|
||||
await RouteHashPreferences.setHashSize(section.routeHashSize!);
|
||||
}
|
||||
if (section.imageMaxSize != null) {
|
||||
await ImagePreferences.setMaxSize(section.imageMaxSize!);
|
||||
}
|
||||
if (section.imageCompression != null) {
|
||||
await ImagePreferences.setCompression(section.imageCompression!);
|
||||
}
|
||||
if (section.imageGrayscale != null) {
|
||||
await ImagePreferences.setGrayscale(section.imageGrayscale!);
|
||||
}
|
||||
if (section.imageUltraMode != null) {
|
||||
await ImagePreferences.setUltraMode(section.imageUltraMode!);
|
||||
}
|
||||
if (section.showRxTxIndicators != null) {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||
section.showRxTxIndicators!,
|
||||
);
|
||||
}
|
||||
|
||||
final locationTracking = appProvider.locationTrackingService;
|
||||
if (section.fastLocationUpdatesEnabled != null) {
|
||||
locationTracking.fastLocationUpdatesEnabled =
|
||||
section.fastLocationUpdatesEnabled!;
|
||||
}
|
||||
if (section.fastLocationMovementThresholdMeters != null) {
|
||||
locationTracking.fastLocationMovementThresholdMeters =
|
||||
section.fastLocationMovementThresholdMeters!;
|
||||
}
|
||||
if (section.fastLocationActiveCadenceSeconds != null) {
|
||||
locationTracking.fastLocationActiveCadenceSeconds =
|
||||
section.fastLocationActiveCadenceSeconds!;
|
||||
}
|
||||
await locationTracking.saveSettings();
|
||||
await appProvider.reloadProfileScopedSettings();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Background location tracking service for SAR operations
|
||||
/// Tracks user location and sends periodic updates via MeshCore BLE
|
||||
@@ -14,6 +16,11 @@ class BackgroundLocationService {
|
||||
static const String _prefKeyLastLon = 'background_last_lon';
|
||||
|
||||
MeshCoreBleService? _bleService;
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
bool _isInitialized = false;
|
||||
StreamSubscription<Position>? _positionSubscription;
|
||||
|
||||
@@ -58,18 +65,27 @@ class BackgroundLocationService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Platform.isIOS && permission != LocationPermission.always) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission != LocationPermission.always) {
|
||||
debugPrint(
|
||||
'⚠️ [BackgroundLocation] iOS background tracking requires Always permission',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Save settings
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, true);
|
||||
await prefs.setDouble(_prefKeyDistance, distanceThreshold);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), true);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distanceThreshold);
|
||||
|
||||
// Start listening to position updates
|
||||
Position? lastPosition;
|
||||
try {
|
||||
_positionSubscription =
|
||||
Geolocator.getPositionStream(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
locationSettings: _buildLocationSettings(
|
||||
distanceFilter: distanceThreshold.toInt(),
|
||||
),
|
||||
).listen((Position position) async {
|
||||
@@ -100,8 +116,14 @@ class BackgroundLocationService {
|
||||
lastPosition = position;
|
||||
|
||||
// Save to preferences
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLat),
|
||||
position.latitude,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyLastLon),
|
||||
position.longitude,
|
||||
);
|
||||
|
||||
// Update device's advertised location
|
||||
if (_bleService != null && _bleService!.isConnected) {
|
||||
@@ -151,7 +173,7 @@ class BackgroundLocationService {
|
||||
_positionSubscription = null;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, false);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||
debugPrint('✅ [BackgroundLocation] Tracking stopped');
|
||||
}
|
||||
|
||||
@@ -159,16 +181,34 @@ class BackgroundLocationService {
|
||||
/// Note: This will restart tracking with the new threshold
|
||||
Future<void> updateDistanceThreshold(double distance) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyDistance, distance);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyDistance), distance);
|
||||
debugPrint(
|
||||
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
||||
);
|
||||
|
||||
// Restart tracking if currently enabled
|
||||
final isEnabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
||||
final isEnabled = prefs.getBool(_scopedKey(_prefKeyEnabled)) ?? false;
|
||||
if (isEnabled && _bleService != null) {
|
||||
await stopTracking();
|
||||
await startTracking(distanceThreshold: distance);
|
||||
}
|
||||
}
|
||||
|
||||
LocationSettings _buildLocationSettings({int distanceFilter = 10}) {
|
||||
if (Platform.isIOS) {
|
||||
return AppleSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
activityType: ActivityType.fitness,
|
||||
allowBackgroundLocationUpdates: true,
|
||||
distanceFilter: distanceFilter,
|
||||
pauseLocationUpdatesAutomatically: false,
|
||||
showBackgroundLocationIndicator: true,
|
||||
);
|
||||
}
|
||||
|
||||
return LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
distanceFilter: distanceFilter,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'dart:io' show Platform;
|
||||
import 'package:flutter/foundation.dart' show TargetPlatform, defaultTargetPlatform, debugPrint, kIsWeb;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// Service for accessing build information from native platform code
|
||||
/// Currently supports Android only - returns "unknown" for other platforms
|
||||
@@ -25,7 +24,7 @@ class BuildInfoService {
|
||||
}
|
||||
|
||||
// Only Android has the platform channel implementation
|
||||
if (!Platform.isAndroid) {
|
||||
if (kIsWeb || defaultTargetPlatform != TargetPlatform.android) {
|
||||
debugPrint('[BuildInfoService] Not on Android platform, returning "unknown"');
|
||||
_cachedCommitHash = 'unknown';
|
||||
return _cachedCommitHash!;
|
||||
|
||||
@@ -5,6 +5,76 @@ import 'package:meshcore_client/meshcore_client.dart';
|
||||
/// Cayenne LPP (Low Power Payload) data parser
|
||||
/// Used for decoding telemetry sensor data from MeshCore devices
|
||||
class CayenneLppParser {
|
||||
static const int _selfTelemetryChannel = 1;
|
||||
static const int _lppGenericSensor = 100;
|
||||
static const int _lppCurrent = 117;
|
||||
static const int _lppFrequency = 118;
|
||||
static const int _lppPercentage = 120;
|
||||
static const int _lppAltitude = 121;
|
||||
static const int _lppConcentration = 125;
|
||||
static const int _lppPower = 128;
|
||||
static const int _lppSpeed = 129;
|
||||
static const int _lppDistance = 130;
|
||||
static const int _lppEnergy = 131;
|
||||
static const int _lppDirection = 132;
|
||||
static const int _lppUnixTime = 133;
|
||||
static const int _lppColour = 135;
|
||||
static const int _lppGust = 137;
|
||||
static const int _lppDewPoint = 138;
|
||||
static const int _lppRain = 139;
|
||||
static const int _lppSwitch = 142;
|
||||
static const int _lppBinaryBool = 143;
|
||||
static const int _lppBinaryPowerSwitch = 144;
|
||||
static const int _lppBinaryOpen = 145;
|
||||
static const int _lppBinaryBatteryLow = 146;
|
||||
static const int _lppBinaryCharging = 147;
|
||||
static const int _lppBinaryCarbonMonoxide = 148;
|
||||
static const int _lppBinaryCold = 149;
|
||||
static const int _lppBinaryConnectivity = 150;
|
||||
static const int _lppBinaryDoor = 151;
|
||||
static const int _lppBinaryGarageDoor = 152;
|
||||
static const int _lppBinaryGas = 153;
|
||||
static const int _lppBinaryHeat = 154;
|
||||
static const int _lppBinaryLight = 155;
|
||||
static const int _lppBinaryLock = 156;
|
||||
static const int _lppBinaryMoisture = 157;
|
||||
static const int _lppBinaryMotion = 158;
|
||||
static const int _lppBinaryMoving = 159;
|
||||
static const int _lppBinaryOccupancy = 160;
|
||||
static const int _lppBinaryPlug = 161;
|
||||
static const int _lppBinaryPresence = 162;
|
||||
static const int _lppBinaryProblem = 163;
|
||||
static const int _lppBinaryRunning = 164;
|
||||
static const int _lppBinarySafety = 165;
|
||||
static const int _lppBinarySmoke = 166;
|
||||
static const int _lppBinarySound = 167;
|
||||
static const int _lppBinaryTamper = 168;
|
||||
static const int _lppBinaryVibration = 169;
|
||||
static const int _lppBinaryWindow = 170;
|
||||
static const int _lppButtonEvent = 171;
|
||||
static const int _lppDimmer = 172;
|
||||
static const int _lppUv = 173;
|
||||
static const int _lppLightLevel = 174;
|
||||
static const int _lppPm25 = 175;
|
||||
static const int _lppPm10 = 176;
|
||||
static const int _lppCo2 = 177;
|
||||
static const int _lppTvoc = 178;
|
||||
static const int _lppRpm = 179;
|
||||
static const int _lppConductivity = 180;
|
||||
static const int _lppRotation = 181;
|
||||
static const int _lppDuration = 182;
|
||||
static const int _lppAcceleration = 183;
|
||||
static const int _lppGyroRate = 184;
|
||||
static const int _lppVolume = 185;
|
||||
static const int _lppFlowRate = 186;
|
||||
static const int _lppVolumeStorage = 187;
|
||||
static const int _lppWater = 188;
|
||||
static const int _lppGasVolume = 189;
|
||||
static const int _lppMass = 190;
|
||||
static const int _lppSignedSpeed = 191;
|
||||
static const int _lppSignedPower = 192;
|
||||
static const int _lppSignedCurrent = 193;
|
||||
|
||||
/// Parse Cayenne LPP data into ContactTelemetry
|
||||
static ContactTelemetry parse(Uint8List data) {
|
||||
debugPrint(' [CayenneLPP] Parsing LPP data...');
|
||||
@@ -25,7 +95,8 @@ class CayenneLppParser {
|
||||
|
||||
int fieldCount = 0;
|
||||
while (reader.hasRemaining) {
|
||||
if (fieldCount > 0 && _isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
||||
if (fieldCount > 0 &&
|
||||
_isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
||||
debugPrint(
|
||||
' Detected zero-padded telemetry tail, stopping parse at position '
|
||||
'${data.length - reader.remainingBytesCount}',
|
||||
@@ -65,14 +136,16 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Analog Input (raw): $rawValue');
|
||||
debugPrint(' Analog Input (volts): ${value}V');
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
// If this is a battery reading
|
||||
if (channel == 0 || channel == 1) {
|
||||
if (_isBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else {
|
||||
extraSensorData['analog_input_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -85,7 +158,7 @@ class CayenneLppParser {
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppIlluminanceSensor:
|
||||
final value = reader.readUInt16BE();
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Illuminance: $value lux');
|
||||
extraSensorData['illuminance_$channel'] = value;
|
||||
break;
|
||||
@@ -98,18 +171,36 @@ class CayenneLppParser {
|
||||
|
||||
case MeshCoreConstants.lppTemperatureSensor:
|
||||
final rawValue = reader.readInt16BE();
|
||||
temperature = rawValue / 10.0;
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Temperature (raw): $rawValue');
|
||||
debugPrint(
|
||||
' Temperature: ${temperature.toStringAsFixed(1)}°C',
|
||||
);
|
||||
debugPrint(' Temperature: ${value.toStringAsFixed(1)}°C');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
temperature = value;
|
||||
extraSensorData[_sourceChannelKey('temperature')] = channel;
|
||||
} else {
|
||||
extraSensorData['temperature_$channel'] = value;
|
||||
if (temperature == null) {
|
||||
temperature = value;
|
||||
extraSensorData[_sourceChannelKey('temperature')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppHumiditySensor:
|
||||
final rawValue = reader.readByte();
|
||||
humidity = rawValue / 2.0;
|
||||
final value = rawValue / 2.0;
|
||||
debugPrint(' Humidity (raw): $rawValue');
|
||||
debugPrint(' Humidity: ${humidity.toStringAsFixed(1)}%');
|
||||
debugPrint(' Humidity: ${value.toStringAsFixed(1)}%');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
humidity = value;
|
||||
extraSensorData[_sourceChannelKey('humidity')] = channel;
|
||||
} else {
|
||||
extraSensorData['humidity_$channel'] = value;
|
||||
if (humidity == null) {
|
||||
humidity = value;
|
||||
extraSensorData[_sourceChannelKey('humidity')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
@@ -126,9 +217,19 @@ class CayenneLppParser {
|
||||
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
pressure = rawValue / 10.0;
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Barometer (raw): $rawValue');
|
||||
debugPrint(' Barometer: ${pressure.toStringAsFixed(1)} hPa');
|
||||
debugPrint(' Barometer: ${value.toStringAsFixed(1)} hPa');
|
||||
if (channel == _selfTelemetryChannel) {
|
||||
pressure = value;
|
||||
extraSensorData[_sourceChannelKey('pressure')] = channel;
|
||||
} else {
|
||||
extraSensorData['pressure_$channel'] = value;
|
||||
if (pressure == null) {
|
||||
pressure = value;
|
||||
extraSensorData[_sourceChannelKey('pressure')] = channel;
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
@@ -136,12 +237,17 @@ class CayenneLppParser {
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Voltage (raw): $rawValue');
|
||||
debugPrint(' Voltage: ${value}V');
|
||||
// Treat voltage sensor as battery reading
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
if (_isBatteryChannel(channel)) {
|
||||
batteryMilliVolts = value * 1000;
|
||||
batteryPercentage = _calculateBatteryPercentage(value);
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||
debugPrint(
|
||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||
);
|
||||
} else {
|
||||
extraSensorData['voltage_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
@@ -194,15 +300,328 @@ class CayenneLppParser {
|
||||
}
|
||||
|
||||
gpsLocation = LatLng(lat, lon);
|
||||
extraSensorData[_sourceChannelKey('gps')] = channel;
|
||||
extraSensorData['altitude_$channel'] = alt;
|
||||
break;
|
||||
|
||||
case _lppGenericSensor:
|
||||
final value = _readUInt32BE(reader).toDouble();
|
||||
debugPrint(' Generic Sensor: $value');
|
||||
extraSensorData['generic_sensor_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppCurrent:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Current (raw): $rawValue');
|
||||
debugPrint(' Current: ${value}A');
|
||||
extraSensorData['current_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppFrequency:
|
||||
final value = _readUInt32BE(reader).toDouble();
|
||||
debugPrint(' Frequency: ${value}Hz');
|
||||
extraSensorData['frequency_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPercentage:
|
||||
final value = reader.readByte().toDouble();
|
||||
debugPrint(' Percentage: $value%');
|
||||
if (_isBatteryChannel(channel)) {
|
||||
batteryPercentage = value;
|
||||
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||
} else {
|
||||
extraSensorData['percentage_$channel'] = value;
|
||||
}
|
||||
break;
|
||||
|
||||
case _lppAltitude:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue.toDouble();
|
||||
debugPrint(' Altitude: ${value}m');
|
||||
extraSensorData['altitude_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppConcentration:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Concentration: ${value}ppm');
|
||||
extraSensorData['concentration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPower:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Power: ${value}W');
|
||||
extraSensorData['power_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSpeed:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Speed: ${value}m/s');
|
||||
extraSensorData['speed_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDistance:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Distance: ${value}m');
|
||||
extraSensorData['distance_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppEnergy:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Energy: ${value}kWh');
|
||||
extraSensorData['energy_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDirection:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Direction: $value°');
|
||||
extraSensorData['direction_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppUnixTime:
|
||||
final value = _readUInt32BE(reader);
|
||||
debugPrint(' Unix time: $value');
|
||||
extraSensorData['unixtime_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppColour:
|
||||
final red = reader.readByte();
|
||||
final green = reader.readByte();
|
||||
final blue = reader.readByte();
|
||||
debugPrint(' Colour: r=$red, g=$green, b=$blue');
|
||||
extraSensorData['colour_$channel'] = {
|
||||
'r': red,
|
||||
'g': green,
|
||||
'b': blue,
|
||||
};
|
||||
break;
|
||||
|
||||
case _lppGust:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Gust: ${value}m/s');
|
||||
extraSensorData['gust_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDewPoint:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Dew point: ${value.toStringAsFixed(1)}°C');
|
||||
extraSensorData['dew_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRain:
|
||||
final rawValue = reader.readUInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Rain: ${value}mm');
|
||||
extraSensorData['rain_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppBinaryBool:
|
||||
case _lppBinaryPowerSwitch:
|
||||
case _lppBinaryOpen:
|
||||
case _lppBinaryBatteryLow:
|
||||
case _lppBinaryCharging:
|
||||
case _lppBinaryCarbonMonoxide:
|
||||
case _lppBinaryCold:
|
||||
case _lppBinaryConnectivity:
|
||||
case _lppBinaryDoor:
|
||||
case _lppBinaryGarageDoor:
|
||||
case _lppBinaryGas:
|
||||
case _lppBinaryHeat:
|
||||
case _lppBinaryLight:
|
||||
case _lppBinaryLock:
|
||||
case _lppBinaryMoisture:
|
||||
case _lppBinaryMotion:
|
||||
case _lppBinaryMoving:
|
||||
case _lppBinaryOccupancy:
|
||||
case _lppBinaryPlug:
|
||||
case _lppBinaryPresence:
|
||||
case _lppBinaryProblem:
|
||||
case _lppBinaryRunning:
|
||||
case _lppBinarySafety:
|
||||
case _lppBinarySmoke:
|
||||
case _lppBinarySound:
|
||||
case _lppBinaryTamper:
|
||||
case _lppBinaryVibration:
|
||||
case _lppBinaryWindow:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Binary state: $value');
|
||||
extraSensorData['${_binaryMetricKeyForType(type)}_$channel'] =
|
||||
value;
|
||||
break;
|
||||
|
||||
case _lppButtonEvent:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Button event: $value');
|
||||
extraSensorData['button_event_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDimmer:
|
||||
final value = _readInt8(reader);
|
||||
debugPrint(' Dimmer: $value');
|
||||
extraSensorData['dimmer_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppUv:
|
||||
final value = reader.readByte() / 10.0;
|
||||
debugPrint(' UV index: $value');
|
||||
extraSensorData['uv_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppLightLevel:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Light level: $value');
|
||||
extraSensorData['light_level_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPm25:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' PM2.5: $value');
|
||||
extraSensorData['pm25_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppPm10:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' PM10: $value');
|
||||
extraSensorData['pm10_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppCo2:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' CO2: $value');
|
||||
extraSensorData['co2_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppTvoc:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' TVOC: $value');
|
||||
extraSensorData['tvoc_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRpm:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' RPM: $value');
|
||||
extraSensorData['rpm_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppConductivity:
|
||||
final value = reader.readUInt16BE().toDouble();
|
||||
debugPrint(' Conductivity: $value');
|
||||
extraSensorData['conductivity_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppRotation:
|
||||
final rawValue = reader.readInt16BE();
|
||||
final value = rawValue / 10.0;
|
||||
debugPrint(' Rotation: $value');
|
||||
extraSensorData['rotation_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppDuration:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Duration: $value s');
|
||||
extraSensorData['duration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppAcceleration:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000000.0;
|
||||
debugPrint(' Acceleration: $value');
|
||||
extraSensorData['acceleration_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppGyroRate:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Gyro rate: $value');
|
||||
extraSensorData['gyro_rate_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppVolume:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Volume: $value');
|
||||
extraSensorData['volume_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppFlowRate:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Flow rate: $value');
|
||||
extraSensorData['flow_rate_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppVolumeStorage:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Storage volume: $value');
|
||||
extraSensorData['volume_storage_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppWater:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Water: $value');
|
||||
extraSensorData['water_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppGasVolume:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Gas volume: $value');
|
||||
extraSensorData['gas_volume_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppMass:
|
||||
final rawValue = _readUInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Mass: $value');
|
||||
extraSensorData['mass_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedSpeed:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000000.0;
|
||||
debugPrint(' Signed speed: $value');
|
||||
extraSensorData['signed_speed_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedPower:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 100.0;
|
||||
debugPrint(' Signed power: $value');
|
||||
extraSensorData['signed_power_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSignedCurrent:
|
||||
final rawValue = _readInt32BE(reader);
|
||||
final value = rawValue / 1000.0;
|
||||
debugPrint(' Signed current: $value');
|
||||
extraSensorData['signed_current_$channel'] = value;
|
||||
break;
|
||||
|
||||
case _lppSwitch:
|
||||
final value = reader.readByte();
|
||||
debugPrint(' Switch: $value');
|
||||
extraSensorData['switch_$channel'] = value;
|
||||
break;
|
||||
|
||||
default:
|
||||
debugPrint(
|
||||
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
|
||||
);
|
||||
// Unknown type, skip remaining to avoid parsing errors
|
||||
reader.skip(reader.remainingBytesCount);
|
||||
final size = _payloadSizeForType(type);
|
||||
if (size == null || reader.remainingBytesCount < size) {
|
||||
debugPrint(
|
||||
' ⚠️ Unknown type $type with unsupported size, stopping parse',
|
||||
);
|
||||
reader.skip(reader.remainingBytesCount);
|
||||
break;
|
||||
}
|
||||
debugPrint(' ⚠️ Unknown type $type, skipping $size bytes');
|
||||
reader.skip(size);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
@@ -258,6 +677,163 @@ class CayenneLppParser {
|
||||
return ((voltage - 3.0) / 1.2) * 100.0;
|
||||
}
|
||||
|
||||
static bool _isBatteryChannel(int channel) =>
|
||||
channel == 0 || channel == _selfTelemetryChannel;
|
||||
|
||||
static int _readUInt32BE(BufferReader reader) {
|
||||
final bytes = reader.readBytes(4);
|
||||
return (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3];
|
||||
}
|
||||
|
||||
static int _readInt32BE(BufferReader reader) {
|
||||
final value = _readUInt32BE(reader);
|
||||
if ((value & 0x80000000) != 0) {
|
||||
return value - 0x100000000;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static int _readInt8(BufferReader reader) {
|
||||
final value = reader.readByte();
|
||||
if ((value & 0x80) != 0) {
|
||||
return value - 0x100;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
static String _sourceChannelKey(String fieldKey) =>
|
||||
'__source_channel:$fieldKey';
|
||||
|
||||
static String _binaryMetricKeyForType(int type) {
|
||||
switch (type) {
|
||||
case _lppBinaryBool:
|
||||
return 'binary_bool';
|
||||
case _lppBinaryPowerSwitch:
|
||||
return 'binary_power_switch';
|
||||
case _lppBinaryOpen:
|
||||
return 'binary_open';
|
||||
case _lppBinaryBatteryLow:
|
||||
return 'binary_battery_low';
|
||||
case _lppBinaryCharging:
|
||||
return 'binary_charging';
|
||||
case _lppBinaryCarbonMonoxide:
|
||||
return 'binary_carbon_monoxide';
|
||||
case _lppBinaryCold:
|
||||
return 'binary_cold';
|
||||
case _lppBinaryConnectivity:
|
||||
return 'binary_connectivity';
|
||||
case _lppBinaryDoor:
|
||||
return 'binary_door';
|
||||
case _lppBinaryGarageDoor:
|
||||
return 'binary_garage_door';
|
||||
case _lppBinaryGas:
|
||||
return 'binary_gas';
|
||||
case _lppBinaryHeat:
|
||||
return 'binary_heat';
|
||||
case _lppBinaryLight:
|
||||
return 'binary_light';
|
||||
case _lppBinaryLock:
|
||||
return 'binary_lock';
|
||||
case _lppBinaryMoisture:
|
||||
return 'binary_moisture';
|
||||
case _lppBinaryMotion:
|
||||
return 'binary_motion';
|
||||
case _lppBinaryMoving:
|
||||
return 'binary_moving';
|
||||
case _lppBinaryOccupancy:
|
||||
return 'binary_occupancy';
|
||||
case _lppBinaryPlug:
|
||||
return 'binary_plug';
|
||||
case _lppBinaryPresence:
|
||||
return 'binary_presence';
|
||||
case _lppBinaryProblem:
|
||||
return 'binary_problem';
|
||||
case _lppBinaryRunning:
|
||||
return 'binary_running';
|
||||
case _lppBinarySafety:
|
||||
return 'binary_safety';
|
||||
case _lppBinarySmoke:
|
||||
return 'binary_smoke';
|
||||
case _lppBinarySound:
|
||||
return 'binary_sound';
|
||||
case _lppBinaryTamper:
|
||||
return 'binary_tamper';
|
||||
case _lppBinaryVibration:
|
||||
return 'binary_vibration';
|
||||
case _lppBinaryWindow:
|
||||
return 'binary_window';
|
||||
}
|
||||
return 'binary_state';
|
||||
}
|
||||
|
||||
static int? _payloadSizeForType(int type) {
|
||||
if (type >= _lppBinaryBool && type <= _lppBinaryWindow) {
|
||||
return 1;
|
||||
}
|
||||
switch (type) {
|
||||
case MeshCoreConstants.lppDigitalInput:
|
||||
case MeshCoreConstants.lppDigitalOutput:
|
||||
case MeshCoreConstants.lppPresenceSensor:
|
||||
case MeshCoreConstants.lppHumiditySensor:
|
||||
case _lppPercentage:
|
||||
case _lppSwitch:
|
||||
case _lppButtonEvent:
|
||||
case _lppDimmer:
|
||||
case _lppUv:
|
||||
case _lppLightLevel:
|
||||
return 1;
|
||||
case MeshCoreConstants.lppAnalogInput:
|
||||
case MeshCoreConstants.lppAnalogOutput:
|
||||
case MeshCoreConstants.lppIlluminanceSensor:
|
||||
case MeshCoreConstants.lppTemperatureSensor:
|
||||
case MeshCoreConstants.lppBarometer:
|
||||
case MeshCoreConstants.lppVoltageSensor:
|
||||
case _lppCurrent:
|
||||
case _lppAltitude:
|
||||
case _lppConcentration:
|
||||
case _lppPower:
|
||||
case _lppSpeed:
|
||||
case _lppDirection:
|
||||
case _lppGust:
|
||||
case _lppDewPoint:
|
||||
case _lppRain:
|
||||
case _lppPm25:
|
||||
case _lppPm10:
|
||||
case _lppCo2:
|
||||
case _lppTvoc:
|
||||
case _lppRpm:
|
||||
case _lppConductivity:
|
||||
case _lppRotation:
|
||||
return 2;
|
||||
case MeshCoreConstants.lppAccelerometer:
|
||||
case MeshCoreConstants.lppGyrometer:
|
||||
return 6;
|
||||
case MeshCoreConstants.lppGps:
|
||||
return 9;
|
||||
case _lppGenericSensor:
|
||||
case _lppFrequency:
|
||||
case _lppDistance:
|
||||
case _lppEnergy:
|
||||
case _lppUnixTime:
|
||||
case _lppDuration:
|
||||
case _lppAcceleration:
|
||||
case _lppGyroRate:
|
||||
case _lppVolume:
|
||||
case _lppFlowRate:
|
||||
case _lppVolumeStorage:
|
||||
case _lppWater:
|
||||
case _lppGasVolume:
|
||||
case _lppMass:
|
||||
case _lppSignedSpeed:
|
||||
case _lppSignedPower:
|
||||
case _lppSignedCurrent:
|
||||
return 4;
|
||||
case _lppColour:
|
||||
return 3;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
|
||||
final start = data.length - remainingBytes;
|
||||
for (int i = start; i < data.length; i++) {
|
||||
|
||||
336
lib/services/contact_route_resolver.dart
Normal file
336
lib/services/contact_route_resolver.dart
Normal file
@@ -0,0 +1,336 @@
|
||||
import 'dart:math' as math;
|
||||
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
import '../models/contact.dart';
|
||||
|
||||
class ResolvedContactRoutePlan {
|
||||
final List<String> tokens;
|
||||
final List<Contact> selectedContacts;
|
||||
final String summary;
|
||||
|
||||
const ResolvedContactRoutePlan({
|
||||
required this.tokens,
|
||||
required this.selectedContacts,
|
||||
required this.summary,
|
||||
});
|
||||
|
||||
String get canonicalText => tokens.join(',');
|
||||
}
|
||||
|
||||
class ContactRouteResolver {
|
||||
static const Distance _distance = Distance();
|
||||
|
||||
const ContactRouteResolver._();
|
||||
|
||||
static ResolvedContactRoutePlan? resolveAutomaticRoute({
|
||||
required LatLng senderLocation,
|
||||
required Contact recipient,
|
||||
required List<Contact> availableContacts,
|
||||
required int hashSize,
|
||||
}) {
|
||||
final recipientLocation = recipient.displayLocation;
|
||||
if (recipientLocation == null) return null;
|
||||
|
||||
final repeaters = availableContacts
|
||||
.where(
|
||||
(contact) =>
|
||||
contact.isRepeater &&
|
||||
contact.displayLocation != null &&
|
||||
contact.publicKeyHex != recipient.publicKeyHex,
|
||||
)
|
||||
.toList();
|
||||
if (repeaters.isEmpty) return null;
|
||||
|
||||
final recipientLatLng = LatLng(
|
||||
recipientLocation.latitude,
|
||||
recipientLocation.longitude,
|
||||
);
|
||||
|
||||
final routedCandidates =
|
||||
repeaters
|
||||
.where(
|
||||
(contact) =>
|
||||
contact.routeHasPath && contact.routeHashSize == hashSize,
|
||||
)
|
||||
.toList()
|
||||
..sort(
|
||||
(a, b) =>
|
||||
_scoreKnownRouteRepeater(
|
||||
senderLocation: senderLocation,
|
||||
recipientLocation: recipientLatLng,
|
||||
repeater: a,
|
||||
availableContacts: repeaters,
|
||||
hashSize: hashSize,
|
||||
).compareTo(
|
||||
_scoreKnownRouteRepeater(
|
||||
senderLocation: senderLocation,
|
||||
recipientLocation: recipientLatLng,
|
||||
repeater: b,
|
||||
availableContacts: repeaters,
|
||||
hashSize: hashSize,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (routedCandidates.isNotEmpty) {
|
||||
final anchor = routedCandidates.first;
|
||||
final tokens = <String>[
|
||||
...anchor.routeCanonicalText
|
||||
.split(',')
|
||||
.where((token) => token.isNotEmpty)
|
||||
.map((token) => token.toUpperCase()),
|
||||
_tokenFor(anchor, hashSize),
|
||||
];
|
||||
final selectedContacts = _matchContactsForTokens(
|
||||
tokens,
|
||||
availableContacts: repeaters,
|
||||
);
|
||||
return ResolvedContactRoutePlan(
|
||||
tokens: _dedupeTokens(tokens),
|
||||
selectedContacts: selectedContacts,
|
||||
summary: 'Resolved via known repeater route',
|
||||
);
|
||||
}
|
||||
|
||||
final corridorRepeaters = List<Contact>.from(repeaters)
|
||||
..sort((a, b) {
|
||||
final progressA = _progressAlongSegment(
|
||||
point: LatLng(
|
||||
a.displayLocation!.latitude,
|
||||
a.displayLocation!.longitude,
|
||||
),
|
||||
start: senderLocation,
|
||||
end: recipientLatLng,
|
||||
);
|
||||
final progressB = _progressAlongSegment(
|
||||
point: LatLng(
|
||||
b.displayLocation!.latitude,
|
||||
b.displayLocation!.longitude,
|
||||
),
|
||||
start: senderLocation,
|
||||
end: recipientLatLng,
|
||||
);
|
||||
final progressCompare = progressA.compareTo(progressB);
|
||||
if (progressCompare != 0) return progressCompare;
|
||||
return _scoreRepeater(
|
||||
senderLocation: senderLocation,
|
||||
recipientLocation: recipientLatLng,
|
||||
repeater: a,
|
||||
).compareTo(
|
||||
_scoreRepeater(
|
||||
senderLocation: senderLocation,
|
||||
recipientLocation: recipientLatLng,
|
||||
repeater: b,
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
final selected = <Contact>[];
|
||||
var currentPoint = senderLocation;
|
||||
var currentDistanceToRecipient = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
senderLocation,
|
||||
recipientLatLng,
|
||||
);
|
||||
final maxCorridorDistance = math.max(
|
||||
1500.0,
|
||||
currentDistanceToRecipient * 0.22,
|
||||
);
|
||||
|
||||
for (final repeater in corridorRepeaters) {
|
||||
if (selected.length >= 4) break;
|
||||
final repeaterPoint = LatLng(
|
||||
repeater.displayLocation!.latitude,
|
||||
repeater.displayLocation!.longitude,
|
||||
);
|
||||
final distanceToSegment = _distanceToSegmentMeters(
|
||||
repeaterPoint,
|
||||
senderLocation,
|
||||
recipientLatLng,
|
||||
);
|
||||
if (distanceToSegment > maxCorridorDistance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final nextDistanceToRecipient = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
repeaterPoint,
|
||||
recipientLatLng,
|
||||
);
|
||||
if (nextDistanceToRecipient >= currentDistanceToRecipient - 300) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final distanceFromCurrent = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
currentPoint,
|
||||
repeaterPoint,
|
||||
);
|
||||
if (distanceFromCurrent < 100) {
|
||||
continue;
|
||||
}
|
||||
|
||||
selected.add(repeater);
|
||||
currentPoint = repeaterPoint;
|
||||
currentDistanceToRecipient = nextDistanceToRecipient;
|
||||
|
||||
if (currentDistanceToRecipient < 2500) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (selected.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return ResolvedContactRoutePlan(
|
||||
tokens: selected.map((contact) => _tokenFor(contact, hashSize)).toList(),
|
||||
selectedContacts: selected,
|
||||
summary: 'Resolved from repeater locations',
|
||||
);
|
||||
}
|
||||
|
||||
static List<Contact> _matchContactsForTokens(
|
||||
List<String> tokens, {
|
||||
required List<Contact> availableContacts,
|
||||
}) {
|
||||
final matches = <Contact>[];
|
||||
final seen = <String>{};
|
||||
for (final token in tokens) {
|
||||
final match = availableContacts
|
||||
.where(
|
||||
(contact) => contact.publicKeyHex.toUpperCase().startsWith(token),
|
||||
)
|
||||
.firstOrNull;
|
||||
if (match != null && seen.add(match.publicKeyHex)) {
|
||||
matches.add(match);
|
||||
}
|
||||
}
|
||||
return matches;
|
||||
}
|
||||
|
||||
static List<String> _dedupeTokens(List<String> tokens) {
|
||||
final result = <String>[];
|
||||
for (final token in tokens) {
|
||||
if (result.isEmpty || result.last != token) {
|
||||
result.add(token);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
static String _tokenFor(Contact contact, int hashSize) {
|
||||
final hex = contact.publicKeyHex.toUpperCase();
|
||||
final length = hashSize * 2;
|
||||
return hex.length < length ? hex : hex.substring(0, length);
|
||||
}
|
||||
|
||||
static double _scoreRepeater({
|
||||
required LatLng senderLocation,
|
||||
required LatLng recipientLocation,
|
||||
required Contact repeater,
|
||||
}) {
|
||||
final point = LatLng(
|
||||
repeater.displayLocation!.latitude,
|
||||
repeater.displayLocation!.longitude,
|
||||
);
|
||||
final toRecipient = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
point,
|
||||
recipientLocation,
|
||||
);
|
||||
final corridor = _distanceToSegmentMeters(
|
||||
point,
|
||||
senderLocation,
|
||||
recipientLocation,
|
||||
);
|
||||
return (toRecipient * 0.75) + (corridor * 0.25);
|
||||
}
|
||||
|
||||
static double _scoreKnownRouteRepeater({
|
||||
required LatLng senderLocation,
|
||||
required LatLng recipientLocation,
|
||||
required Contact repeater,
|
||||
required List<Contact> availableContacts,
|
||||
required int hashSize,
|
||||
}) {
|
||||
final baseScore = _scoreRepeater(
|
||||
senderLocation: senderLocation,
|
||||
recipientLocation: recipientLocation,
|
||||
repeater: repeater,
|
||||
);
|
||||
final repeaterPoint = LatLng(
|
||||
repeater.displayLocation!.latitude,
|
||||
repeater.displayLocation!.longitude,
|
||||
);
|
||||
|
||||
final chainContacts = repeater.routeCanonicalText
|
||||
.split(',')
|
||||
.where((token) => token.isNotEmpty)
|
||||
.map(
|
||||
(token) => availableContacts
|
||||
.where(
|
||||
(contact) =>
|
||||
contact.displayLocation != null &&
|
||||
contact.publicKeyHex.toUpperCase().startsWith(
|
||||
token.toUpperCase(),
|
||||
),
|
||||
)
|
||||
.firstOrNull,
|
||||
)
|
||||
.whereType<Contact>()
|
||||
.toList();
|
||||
|
||||
final chainEnd = chainContacts.isNotEmpty
|
||||
? LatLng(
|
||||
chainContacts.last.displayLocation!.latitude,
|
||||
chainContacts.last.displayLocation!.longitude,
|
||||
)
|
||||
: senderLocation;
|
||||
final chainGap = _distance.as(LengthUnit.Meter, chainEnd, repeaterPoint);
|
||||
final senderGap = _distance.as(
|
||||
LengthUnit.Meter,
|
||||
senderLocation,
|
||||
repeaterPoint,
|
||||
);
|
||||
|
||||
return (chainGap * 0.55) + (baseScore * 0.35) + (senderGap * 0.10);
|
||||
}
|
||||
|
||||
static double _progressAlongSegment({
|
||||
required LatLng point,
|
||||
required LatLng start,
|
||||
required LatLng end,
|
||||
}) {
|
||||
final dx = end.longitude - start.longitude;
|
||||
final dy = end.latitude - start.latitude;
|
||||
final lengthSquared = (dx * dx) + (dy * dy);
|
||||
if (lengthSquared == 0) return 0;
|
||||
return (((point.longitude - start.longitude) * dx) +
|
||||
((point.latitude - start.latitude) * dy)) /
|
||||
lengthSquared;
|
||||
}
|
||||
|
||||
static double _distanceToSegmentMeters(LatLng p, LatLng a, LatLng b) {
|
||||
final ax = a.longitude;
|
||||
final ay = a.latitude;
|
||||
final bx = b.longitude;
|
||||
final by = b.latitude;
|
||||
final px = p.longitude;
|
||||
final py = p.latitude;
|
||||
|
||||
final abx = bx - ax;
|
||||
final aby = by - ay;
|
||||
final apx = px - ax;
|
||||
final apy = py - ay;
|
||||
final ab2 = abx * abx + aby * aby;
|
||||
if (ab2 == 0) {
|
||||
return _distance.as(LengthUnit.Meter, a, p);
|
||||
}
|
||||
var t = (apx * abx + apy * aby) / ab2;
|
||||
t = t.clamp(0.0, 1.0);
|
||||
final closest = LatLng(ay + aby * t, ax + abx * t);
|
||||
return _distance.as(LengthUnit.Meter, closest, p);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,25 @@ import 'dart:convert';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/contact_group.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import '../utils/key_comparison.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting contacts to local storage
|
||||
class ContactStorageService {
|
||||
static const String _contactsKey = 'stored_contacts';
|
||||
static const String _contactGroupsKey = 'stored_contact_groups';
|
||||
static const String _pendingAdvertsKey = 'stored_pending_adverts';
|
||||
static const int _maxStoredContacts = 500; // Store up to 500 contacts
|
||||
static const int _maxStoredPendingAdverts = 500;
|
||||
|
||||
String _key(String baseKey, {String? namespace}) {
|
||||
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
|
||||
}
|
||||
|
||||
/// Save contacts to persistent storage
|
||||
Future<void> saveContacts(List<Contact> contacts) async {
|
||||
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
@@ -26,7 +35,10 @@ class ContactStorageService {
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_contactsKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
||||
@@ -38,10 +50,15 @@ class ContactStorageService {
|
||||
|
||||
/// Load contacts from persistent storage
|
||||
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
|
||||
Future<List<Contact>> loadContacts({Uint8List? excludePublicKey}) async {
|
||||
Future<List<Contact>> loadContacts({
|
||||
Uint8List? excludePublicKey,
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
debugPrint('ℹ️ [ContactStorage] No stored contacts found');
|
||||
@@ -80,21 +97,158 @@ class ContactStorageService {
|
||||
}
|
||||
|
||||
/// Clear all stored contacts
|
||||
Future<void> clearContacts() async {
|
||||
Future<void> clearContacts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_contactsKey);
|
||||
await prefs.remove(_key(_contactsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
Future<void> saveContactGroups(
|
||||
List<SavedContactGroup> groups, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_contactsKey);
|
||||
final jsonString = jsonEncode(
|
||||
groups.map((group) => _contactGroupToJson(group)).toList(),
|
||||
);
|
||||
await prefs.setString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving contact groups: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<SavedContactGroup>> loadContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactGroupsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
return jsonList
|
||||
.map((json) => _contactGroupFromJson(json as Map<String, dynamic>))
|
||||
.whereType<SavedContactGroup>()
|
||||
.toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading contact groups: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearContactGroups({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_key(_contactGroupsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all contact groups');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing contact groups: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> savePendingAdverts(
|
||||
List<Map<String, dynamic>> adverts, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final limitedList = adverts.length > _maxStoredPendingAdverts
|
||||
? adverts.sublist(adverts.length - _maxStoredPendingAdverts)
|
||||
: adverts;
|
||||
await prefs.setString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
jsonEncode(limitedList),
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [ContactStorage] Saved ${limitedList.length} pending adverts to storage',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving pending adverts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> loadPendingAdverts({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_pendingAdvertsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||
return jsonList.whereType<Map<String, dynamic>>().toList();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading pending adverts: $e');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> clearPendingAdverts({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_key(_pendingAdvertsKey, namespace: namespace));
|
||||
debugPrint('✅ [ContactStorage] Cleared all stored pending adverts');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error clearing pending adverts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
// ── Favorites ───────────────────────────────────────────────────────────────
|
||||
|
||||
static const String _favoritesKey = 'contact_favorites';
|
||||
|
||||
Future<void> saveFavorites(
|
||||
Set<String> publicKeyHexes, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
publicKeyHexes.toList(),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error saving favorites: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<Set<String>> loadFavorites({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final list = prefs.getStringList(
|
||||
_key(_favoritesKey, namespace: namespace),
|
||||
);
|
||||
return list?.toSet() ?? {};
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error loading favorites: $e');
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_contactsKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
@@ -123,6 +277,7 @@ class ContactStorageService {
|
||||
'outPathLen': contact.outPathLen,
|
||||
'outPath': base64Encode(contact.outPath),
|
||||
'advName': contact.advName,
|
||||
'nameOverride': contact.nameOverride,
|
||||
'lastAdvert': contact.lastAdvert,
|
||||
'advLat': contact.advLat,
|
||||
'advLon': contact.advLon,
|
||||
@@ -130,6 +285,15 @@ class ContactStorageService {
|
||||
'telemetry': contact.telemetry != null
|
||||
? _telemetryToJson(contact.telemetry!)
|
||||
: null,
|
||||
'advertHistory': contact.advertHistory
|
||||
.map(
|
||||
(point) => {
|
||||
'lat': point.location.latitude,
|
||||
'lon': point.location.longitude,
|
||||
'tsMillis': point.timestamp.millisecondsSinceEpoch,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -145,6 +309,7 @@ class ContactStorageService {
|
||||
outPathLen: json['outPathLen'] as int,
|
||||
outPath: Uint8List.fromList(base64Decode(json['outPath'] as String)),
|
||||
advName: json['advName'] as String,
|
||||
nameOverride: json['nameOverride'] as String?,
|
||||
lastAdvert: json['lastAdvert'] as int,
|
||||
advLat: json['advLat'] as int,
|
||||
advLon: json['advLon'] as int,
|
||||
@@ -152,6 +317,7 @@ class ContactStorageService {
|
||||
telemetry: json['telemetry'] != null
|
||||
? _telemetryFromJson(json['telemetry'] as Map<String, dynamic>)
|
||||
: null,
|
||||
advertHistory: _advertHistoryFromJson(json['advertHistory']),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error parsing contact from JSON: $e');
|
||||
@@ -203,4 +369,56 @@ class ContactStorageService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
List<AdvertLocation> _advertHistoryFromJson(dynamic json) {
|
||||
if (json is! List) return [];
|
||||
final result = <AdvertLocation>[];
|
||||
for (final item in json) {
|
||||
if (item is! Map<String, dynamic>) continue;
|
||||
final lat = item['lat'];
|
||||
final lon = item['lon'];
|
||||
final tsMillis = item['tsMillis'];
|
||||
if (lat is! num || lon is! num || tsMillis is! int) continue;
|
||||
result.add(
|
||||
AdvertLocation(
|
||||
location: LatLng(lat.toDouble(), lon.toDouble()),
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(tsMillis),
|
||||
),
|
||||
);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Map<String, dynamic> _contactGroupToJson(SavedContactGroup group) {
|
||||
return {
|
||||
'id': group.id,
|
||||
'sectionKey': group.sectionKey,
|
||||
'label': group.label,
|
||||
'query': group.query,
|
||||
'createdAtMillis': group.createdAt.millisecondsSinceEpoch,
|
||||
'matchPrefixes': group.matchPrefixes,
|
||||
'isAutoGroup': group.isAutoGroup,
|
||||
};
|
||||
}
|
||||
|
||||
SavedContactGroup? _contactGroupFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
return SavedContactGroup(
|
||||
id: json['id'] as String,
|
||||
sectionKey: json['sectionKey'] as String,
|
||||
label: json['label'] as String,
|
||||
query: json['query'] as String,
|
||||
createdAt: DateTime.fromMillisecondsSinceEpoch(
|
||||
json['createdAtMillis'] as int,
|
||||
),
|
||||
matchPrefixes: (json['matchPrefixes'] as List<dynamic>?)
|
||||
?.map((value) => value as String)
|
||||
.toList(),
|
||||
isAutoGroup: json['isAutoGroup'] as bool? ?? false,
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [ContactStorage] Error parsing contact group: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
lib/services/device_config_applicator.dart
Normal file
140
lib/services/device_config_applicator.dart
Normal file
@@ -0,0 +1,140 @@
|
||||
import 'dart:typed_data';
|
||||
|
||||
import '../models/channel.dart';
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/channels_provider.dart';
|
||||
import '../providers/connection_provider.dart';
|
||||
|
||||
class DeviceConfigApplicator {
|
||||
ConfigProfileSections capture({
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) {
|
||||
final deviceInfo = connectionProvider.deviceInfo;
|
||||
return ConfigProfileSections(
|
||||
deviceConfig: DeviceConfigProfileSection(
|
||||
frequencyKhz: deviceInfo.radioFreq,
|
||||
bandwidth: deviceInfo.radioBw,
|
||||
spreadingFactor: deviceInfo.radioSf,
|
||||
codingRate: deviceInfo.radioCr,
|
||||
repeatEnabled: deviceInfo.clientRepeat,
|
||||
txPower: deviceInfo.txPower,
|
||||
telemetryModes: deviceInfo.telemetryModes,
|
||||
advertLocationPolicy: deviceInfo.advertLocPolicy,
|
||||
multiAcks: deviceInfo.multiAcks,
|
||||
manualAddContacts: deviceInfo.manualAddContacts,
|
||||
autoAddUsers: deviceInfo.autoAddUsers,
|
||||
autoAddRepeaters: deviceInfo.autoAddRepeaters,
|
||||
autoAddRoomServers: deviceInfo.autoAddRoomServers,
|
||||
autoAddSensors: deviceInfo.autoAddSensors,
|
||||
autoAddOverwriteOldest: deviceInfo.autoAddOverwriteOldest,
|
||||
publicLatitude: _decodeAdvertCoordinate(deviceInfo.advLat),
|
||||
publicLongitude: _decodeAdvertCoordinate(deviceInfo.advLon),
|
||||
),
|
||||
channels: channelsProvider.channels,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
ConfigProfile profile, {
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (!connectionProvider.deviceInfo.isConnected) {
|
||||
return;
|
||||
}
|
||||
|
||||
final deviceConfig = profile.sections.deviceConfig;
|
||||
if (deviceConfig != null && !deviceConfig.isEmpty) {
|
||||
if (deviceConfig.txPower != null) {
|
||||
await connectionProvider.setTxPower(deviceConfig.txPower!);
|
||||
}
|
||||
if (deviceConfig.telemetryModes != null ||
|
||||
deviceConfig.advertLocationPolicy != null ||
|
||||
deviceConfig.manualAddContacts != null ||
|
||||
deviceConfig.multiAcks != null) {
|
||||
await connectionProvider.setOtherParams(
|
||||
manualAddContacts: (deviceConfig.manualAddContacts ?? false) ? 1 : 0,
|
||||
telemetryModes: deviceConfig.telemetryModes ?? 0,
|
||||
advertLocationPolicy: deviceConfig.advertLocationPolicy ?? 0,
|
||||
multiAcks: deviceConfig.multiAcks ?? 0,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.autoAddUsers != null &&
|
||||
deviceConfig.autoAddRepeaters != null &&
|
||||
deviceConfig.autoAddRoomServers != null &&
|
||||
deviceConfig.autoAddSensors != null &&
|
||||
deviceConfig.autoAddOverwriteOldest != null) {
|
||||
await connectionProvider.setAutoaddConfig(
|
||||
autoAddUsers: deviceConfig.autoAddUsers!,
|
||||
autoAddRepeaters: deviceConfig.autoAddRepeaters!,
|
||||
autoAddRoomServers: deviceConfig.autoAddRoomServers!,
|
||||
autoAddSensors: deviceConfig.autoAddSensors!,
|
||||
overwriteOldest: deviceConfig.autoAddOverwriteOldest!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.publicLatitude != null &&
|
||||
deviceConfig.publicLongitude != null) {
|
||||
await connectionProvider.setAdvertLatLon(
|
||||
latitude: deviceConfig.publicLatitude!,
|
||||
longitude: deviceConfig.publicLongitude!,
|
||||
);
|
||||
}
|
||||
if (deviceConfig.frequencyKhz != null &&
|
||||
deviceConfig.bandwidth != null &&
|
||||
deviceConfig.spreadingFactor != null &&
|
||||
deviceConfig.codingRate != null) {
|
||||
await connectionProvider.setRadioParams(
|
||||
frequency: deviceConfig.frequencyKhz!,
|
||||
bandwidth: deviceConfig.bandwidth!,
|
||||
spreadingFactor: deviceConfig.spreadingFactor!,
|
||||
codingRate: deviceConfig.codingRate!,
|
||||
repeat: deviceConfig.repeatEnabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
await _applyChannels(
|
||||
channels: profile.sections.channels,
|
||||
connectionProvider: connectionProvider,
|
||||
channelsProvider: channelsProvider,
|
||||
);
|
||||
await connectionProvider.refreshDeviceInfo();
|
||||
}
|
||||
|
||||
Future<void> _applyChannels({
|
||||
required List<Channel> channels,
|
||||
required ConnectionProvider connectionProvider,
|
||||
required ChannelsProvider channelsProvider,
|
||||
}) async {
|
||||
if (channels.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final desired = {for (final channel in channels) channel.index: channel};
|
||||
|
||||
for (final channel in channels) {
|
||||
await connectionProvider.setChannelSlot(
|
||||
channelIdx: channel.index,
|
||||
channelName: channel.name,
|
||||
secret: Uint8List.fromList(channel.secret),
|
||||
);
|
||||
channelsProvider.addOrUpdateChannelObject(channel);
|
||||
}
|
||||
|
||||
for (final existing in channelsProvider.channels) {
|
||||
if (existing.index == 0 || desired.containsKey(existing.index)) {
|
||||
continue;
|
||||
}
|
||||
await connectionProvider.deleteChannel(existing.index);
|
||||
channelsProvider.removeChannel(existing.index);
|
||||
}
|
||||
}
|
||||
|
||||
double? _decodeAdvertCoordinate(int? value) {
|
||||
if (value == null || value == 0) {
|
||||
return null;
|
||||
}
|
||||
return value / 1e6;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Stores user-selected image compression settings.
|
||||
class ImagePreferences {
|
||||
@@ -17,44 +18,53 @@ class ImagePreferences {
|
||||
|
||||
static Future<int> getMaxSize() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_maxSizeKey) ?? defaultMaxSize;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_maxSizeKey)) ??
|
||||
defaultMaxSize;
|
||||
return supportedSizes.contains(value) ? value : defaultMaxSize;
|
||||
}
|
||||
|
||||
static Future<void> setMaxSize(int size) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_maxSizeKey, size);
|
||||
await prefs.setInt(ProfileStorageScope.scopedKey(_maxSizeKey), size);
|
||||
}
|
||||
|
||||
static Future<int> getCompression() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final value = prefs.getInt(_qualityKey) ?? defaultQuality;
|
||||
final value =
|
||||
prefs.getInt(ProfileStorageScope.scopedKey(_qualityKey)) ??
|
||||
defaultQuality;
|
||||
return value.clamp(10, 90);
|
||||
}
|
||||
|
||||
static Future<void> setCompression(int compression) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setInt(_qualityKey, compression.clamp(10, 90));
|
||||
await prefs.setInt(
|
||||
ProfileStorageScope.scopedKey(_qualityKey),
|
||||
compression.clamp(10, 90),
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getGrayscale() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_grayscaleKey) ?? defaultGrayscale;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_grayscaleKey)) ??
|
||||
defaultGrayscale;
|
||||
}
|
||||
|
||||
static Future<void> setGrayscale(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_grayscaleKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_grayscaleKey), value);
|
||||
}
|
||||
|
||||
static Future<bool> getUltraMode() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_ultraModeKey) ?? defaultUltraMode;
|
||||
return prefs.getBool(ProfileStorageScope.scopedKey(_ultraModeKey)) ??
|
||||
defaultUltraMode;
|
||||
}
|
||||
|
||||
static Future<void> setUltraMode(bool value) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_ultraModeKey, value);
|
||||
await prefs.setBool(ProfileStorageScope.scopedKey(_ultraModeKey), value);
|
||||
}
|
||||
|
||||
static int effectiveMaxSize(
|
||||
|
||||
262
lib/services/live_traffic_summary.dart
Normal file
262
lib/services/live_traffic_summary.dart
Normal file
@@ -0,0 +1,262 @@
|
||||
import '../models/ble_packet_log.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
enum LiveTrafficBusyness { quiet, active, busy }
|
||||
|
||||
class LiveTrafficEntry {
|
||||
final BlePacketLog log;
|
||||
final DecodedLogRxRoute? route;
|
||||
|
||||
const LiveTrafficEntry({required this.log, required this.route});
|
||||
|
||||
bool get isMultiHop => (route?.hopCount ?? 0) > 1;
|
||||
|
||||
int? get hopCount => route?.hopCount;
|
||||
|
||||
String get payloadLabel {
|
||||
final decodedRoute = route;
|
||||
if (decodedRoute == null) {
|
||||
return log.responseCode != null ? log.opcodeName : 'Unknown';
|
||||
}
|
||||
return payloadTypeLabel(decodedRoute.payloadType);
|
||||
}
|
||||
|
||||
String? get payloadMeaning {
|
||||
final decodedRoute = route;
|
||||
if (decodedRoute == null) return null;
|
||||
return payloadTypeMeaning(decodedRoute.payloadType);
|
||||
}
|
||||
|
||||
String get routePreview {
|
||||
final decodedRoute = route;
|
||||
if (decodedRoute == null || decodedRoute.hopHashes.isEmpty) {
|
||||
return 'Direct packet';
|
||||
}
|
||||
return decodedRoute.hopHashes
|
||||
.map((hashHex) => '0x${hashHex.toUpperCase()}')
|
||||
.join(' -> ');
|
||||
}
|
||||
|
||||
static String payloadTypeLabel(int payloadType) {
|
||||
switch (payloadType) {
|
||||
case 0x00:
|
||||
return 'Request';
|
||||
case 0x01:
|
||||
return 'Response';
|
||||
case 0x02:
|
||||
return 'Text message';
|
||||
case 0x03:
|
||||
return 'Ack';
|
||||
case 0x04:
|
||||
return 'Advertisement';
|
||||
case 0x05:
|
||||
return 'Group text';
|
||||
case 0x06:
|
||||
return 'Group datagram';
|
||||
case 0x07:
|
||||
return 'Anonymous request';
|
||||
case 0x08:
|
||||
return 'Returned path';
|
||||
case 0x09:
|
||||
return 'Trace path';
|
||||
case 0x0A:
|
||||
return 'Multipart packet';
|
||||
case 0x0B:
|
||||
return 'Control packet';
|
||||
default:
|
||||
return '0x${payloadType.toRadixString(16).padLeft(2, '0')}';
|
||||
}
|
||||
}
|
||||
|
||||
static String payloadTypeMeaning(int payloadType) {
|
||||
switch (payloadType) {
|
||||
case 0x00:
|
||||
return 'Request (destination/source hashes + MAC)';
|
||||
case 0x01:
|
||||
return 'Response to Request or Anonymous request';
|
||||
case 0x02:
|
||||
return 'Plain text message';
|
||||
case 0x03:
|
||||
return 'Simple acknowledgement';
|
||||
case 0x04:
|
||||
return 'Node advertisement';
|
||||
case 0x05:
|
||||
return 'Unverified group text message';
|
||||
case 0x06:
|
||||
return 'Unverified group datagram';
|
||||
case 0x07:
|
||||
return 'Generic anonymous request';
|
||||
case 0x08:
|
||||
return 'Returned path payload';
|
||||
case 0x09:
|
||||
return 'Trace path collecting hop SNR';
|
||||
case 0x0A:
|
||||
return 'One packet from a multipart set';
|
||||
case 0x0B:
|
||||
return 'Control or discovery packet';
|
||||
default:
|
||||
return 'protocol payload';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class LiveTrafficSnapshot {
|
||||
final DateTime windowStart;
|
||||
final Duration windowDuration;
|
||||
final int packetsPerMinute;
|
||||
final int rxCount;
|
||||
final int txCount;
|
||||
final int totalCount;
|
||||
final double? avgSnrDb;
|
||||
final double? latestSnrDb;
|
||||
final double? avgRssiDbm;
|
||||
final int? latestRssiDbm;
|
||||
final int multiHopCount;
|
||||
final double? avgHopCount;
|
||||
final List<LiveTrafficEntry> visibleEntries;
|
||||
final LiveTrafficBusyness busyness;
|
||||
|
||||
const LiveTrafficSnapshot({
|
||||
required this.windowStart,
|
||||
required this.windowDuration,
|
||||
required this.packetsPerMinute,
|
||||
required this.rxCount,
|
||||
required this.txCount,
|
||||
required this.totalCount,
|
||||
required this.avgSnrDb,
|
||||
required this.latestSnrDb,
|
||||
required this.avgRssiDbm,
|
||||
required this.latestRssiDbm,
|
||||
required this.multiHopCount,
|
||||
required this.avgHopCount,
|
||||
required this.visibleEntries,
|
||||
required this.busyness,
|
||||
});
|
||||
}
|
||||
|
||||
class LiveTrafficSummary {
|
||||
static const Duration rollingWindow = Duration(seconds: 60);
|
||||
static const int maxVisibleEntries = 120;
|
||||
static const int logRxDataResponseCode = 0x88;
|
||||
|
||||
const LiveTrafficSummary._();
|
||||
|
||||
static bool isRxDataLog(BlePacketLog log) {
|
||||
return log.direction == PacketDirection.rx &&
|
||||
log.responseCode == logRxDataResponseCode;
|
||||
}
|
||||
|
||||
static int countRxDataLogs(Iterable<BlePacketLog> logs, {DateTime? since}) {
|
||||
return logs.where((log) {
|
||||
return isRxDataLog(log) &&
|
||||
(since == null || !log.timestamp.isBefore(since));
|
||||
}).length;
|
||||
}
|
||||
|
||||
static LiveTrafficSnapshot fromLogs(
|
||||
Iterable<BlePacketLog> logs, {
|
||||
required DateTime now,
|
||||
DateTime? clearedAt,
|
||||
int? preferredHashSize,
|
||||
Duration window = rollingWindow,
|
||||
String? packetTypeFilter,
|
||||
}) {
|
||||
final windowStart = now.subtract(window);
|
||||
final effectiveStart = clearedAt != null && clearedAt.isAfter(windowStart)
|
||||
? clearedAt
|
||||
: windowStart;
|
||||
|
||||
final recentLogs =
|
||||
logs
|
||||
.where(
|
||||
(log) =>
|
||||
isRxDataLog(log) && !log.timestamp.isBefore(effectiveStart),
|
||||
)
|
||||
.toList()
|
||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||
|
||||
final entries = <LiveTrafficEntry>[];
|
||||
for (final log in recentLogs) {
|
||||
final route = LogRxRouteDecoder.decode(
|
||||
log.rawData,
|
||||
preferredHashSize: preferredHashSize,
|
||||
);
|
||||
entries.add(LiveTrafficEntry(log: log, route: route));
|
||||
}
|
||||
|
||||
final filteredEntries = packetTypeFilter == null
|
||||
? entries
|
||||
: entries
|
||||
.where((entry) => entry.payloadLabel == packetTypeFilter)
|
||||
.toList();
|
||||
|
||||
var rxCount = 0;
|
||||
var snrCount = 0;
|
||||
var snrSum = 0.0;
|
||||
var rssiCount = 0;
|
||||
var rssiSum = 0.0;
|
||||
double? latestSnrDb;
|
||||
int? latestRssiDbm;
|
||||
var multiHopCount = 0;
|
||||
var hopCountTotal = 0;
|
||||
var hopCountSamples = 0;
|
||||
|
||||
for (final entry in filteredEntries) {
|
||||
rxCount += 1;
|
||||
|
||||
final rxInfo = entry.log.logRxDataInfo;
|
||||
if (rxInfo?.snrDb != null) {
|
||||
snrCount += 1;
|
||||
snrSum += rxInfo!.snrDb!;
|
||||
latestSnrDb = rxInfo.snrDb!;
|
||||
}
|
||||
if (rxInfo?.rssiDbm != null) {
|
||||
rssiCount += 1;
|
||||
rssiSum += rxInfo!.rssiDbm!.toDouble();
|
||||
latestRssiDbm = rxInfo.rssiDbm!;
|
||||
}
|
||||
|
||||
final route = entry.route;
|
||||
if (route != null && route.hopCount > 0) {
|
||||
hopCountSamples += 1;
|
||||
hopCountTotal += route.hopCount;
|
||||
if (route.hopCount > 1) {
|
||||
multiHopCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final visibleEntries = filteredEntries.reversed
|
||||
.take(maxVisibleEntries)
|
||||
.toList();
|
||||
const txCount = 0;
|
||||
final totalCount = rxCount;
|
||||
final packetsPerMinute =
|
||||
((totalCount * Duration.secondsPerMinute) / window.inSeconds).round();
|
||||
|
||||
return LiveTrafficSnapshot(
|
||||
windowStart: effectiveStart,
|
||||
windowDuration: window,
|
||||
packetsPerMinute: packetsPerMinute,
|
||||
rxCount: rxCount,
|
||||
txCount: txCount,
|
||||
totalCount: totalCount,
|
||||
avgSnrDb: snrCount == 0 ? null : snrSum / snrCount,
|
||||
latestSnrDb: latestSnrDb,
|
||||
avgRssiDbm: rssiCount == 0 ? null : rssiSum / rssiCount,
|
||||
latestRssiDbm: latestRssiDbm,
|
||||
multiHopCount: multiHopCount,
|
||||
avgHopCount: hopCountSamples == 0
|
||||
? null
|
||||
: hopCountTotal / hopCountSamples,
|
||||
visibleEntries: visibleEntries,
|
||||
busyness: _busynessForPacketsPerMinute(packetsPerMinute),
|
||||
);
|
||||
}
|
||||
|
||||
static LiveTrafficBusyness _busynessForPacketsPerMinute(int ppm) {
|
||||
if (ppm <= 5) return LiveTrafficBusyness.quiet;
|
||||
if (ppm <= 20) return LiveTrafficBusyness.active;
|
||||
return LiveTrafficBusyness.busy;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:meshcore_client/meshcore_client.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
/// Centralized location tracking service for MeshCore SAR
|
||||
///
|
||||
@@ -48,6 +50,10 @@ class LocationTrackingService {
|
||||
static const String _prefKeyFastActiveCadence =
|
||||
'fast_location_active_cadence_seconds';
|
||||
|
||||
String _scopedKey(String baseKey) {
|
||||
return ProfileStorageScope.scopedKey(baseKey);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Configuration Properties
|
||||
// ============================================================================
|
||||
@@ -148,6 +154,10 @@ class LocationTrackingService {
|
||||
/// Check if location permissions are granted
|
||||
Future<bool> checkPermissions() async {
|
||||
final permission = await Geolocator.checkPermission();
|
||||
if (Platform.isIOS) {
|
||||
return permission == LocationPermission.always;
|
||||
}
|
||||
|
||||
return permission == LocationPermission.always ||
|
||||
permission == LocationPermission.whileInUse;
|
||||
}
|
||||
@@ -181,6 +191,16 @@ class LocationTrackingService {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (Platform.isIOS && permission != LocationPermission.always) {
|
||||
permission = await Geolocator.requestPermission();
|
||||
if (permission != LocationPermission.always) {
|
||||
onError?.call(
|
||||
'Background tracking on iOS requires "Always" location access. Enable it in Settings.',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('✅ [LocationTracking] Location permissions granted');
|
||||
return true;
|
||||
}
|
||||
@@ -209,9 +229,9 @@ class LocationTrackingService {
|
||||
}
|
||||
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
locationSettings: _buildLocationSettings(
|
||||
timeLimit: timeLimit,
|
||||
distanceFilter: 0,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -255,13 +275,35 @@ class LocationTrackingService {
|
||||
/// [distanceFilter] - Minimum distance in meters between position updates
|
||||
Stream<Position> getPositionStream({double distanceFilter = 10.0}) {
|
||||
return Geolocator.getPositionStream(
|
||||
locationSettings: LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
locationSettings: _buildLocationSettings(
|
||||
distanceFilter: distanceFilter.toInt(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
LocationSettings _buildLocationSettings({
|
||||
int distanceFilter = 10,
|
||||
Duration? timeLimit,
|
||||
}) {
|
||||
if (Platform.isIOS) {
|
||||
return AppleSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
activityType: ActivityType.fitness,
|
||||
allowBackgroundLocationUpdates: true,
|
||||
distanceFilter: distanceFilter,
|
||||
pauseLocationUpdatesAutomatically: false,
|
||||
showBackgroundLocationIndicator: true,
|
||||
timeLimit: timeLimit,
|
||||
);
|
||||
}
|
||||
|
||||
return LocationSettings(
|
||||
accuracy: LocationAccuracy.best,
|
||||
distanceFilter: distanceFilter,
|
||||
timeLimit: timeLimit,
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tracking Control
|
||||
// ============================================================================
|
||||
@@ -362,7 +404,7 @@ class LocationTrackingService {
|
||||
|
||||
// Save disabled state
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefKeyEnabled, false);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||
|
||||
debugPrint('✅ [LocationTracking] Tracking stopped');
|
||||
}
|
||||
@@ -440,8 +482,8 @@ class LocationTrackingService {
|
||||
|
||||
// Save to preferences
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyLastLat), position.latitude);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyLastLon), position.longitude);
|
||||
|
||||
debugPrint('✅ [LocationTracking] Initial position set without broadcast');
|
||||
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
|
||||
@@ -609,17 +651,24 @@ class LocationTrackingService {
|
||||
Future<void> loadSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
minDistanceMeters = prefs.getDouble(_prefKeyMinDistance) ?? 5.0;
|
||||
maxDistanceMeters = prefs.getDouble(_prefKeyMaxDistance) ?? 100.0;
|
||||
minTimeIntervalSeconds = prefs.getInt(_prefKeyMinTimeInterval) ?? 30;
|
||||
gpsUpdateDistance = prefs.getDouble(_prefKeyGpsUpdateDistance) ?? 10.0;
|
||||
minDistanceMeters = prefs.getDouble(_scopedKey(_prefKeyMinDistance)) ?? 5.0;
|
||||
maxDistanceMeters =
|
||||
prefs.getDouble(_scopedKey(_prefKeyMaxDistance)) ?? 100.0;
|
||||
minTimeIntervalSeconds =
|
||||
prefs.getInt(_scopedKey(_prefKeyMinTimeInterval)) ?? 30;
|
||||
gpsUpdateDistance =
|
||||
prefs.getDouble(_scopedKey(_prefKeyGpsUpdateDistance)) ?? 10.0;
|
||||
fastLocationUpdatesEnabled =
|
||||
prefs.getBool(_prefKeyFastLocationEnabled) ?? false;
|
||||
prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false;
|
||||
fastLocationMovementThresholdMeters =
|
||||
(prefs.getDouble(_prefKeyFastMovementThreshold) ?? gpsUpdateDistance)
|
||||
(prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ??
|
||||
gpsUpdateDistance)
|
||||
.clamp(1.0, 1000.0);
|
||||
fastLocationActiveCadenceSeconds =
|
||||
(prefs.getInt(_prefKeyFastActiveCadence) ?? 10).clamp(5, 60);
|
||||
(prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ?? 10).clamp(
|
||||
5,
|
||||
60,
|
||||
);
|
||||
|
||||
debugPrint('✅ [LocationTracking] Settings loaded');
|
||||
debugPrint(' Min distance: ${minDistanceMeters}m');
|
||||
@@ -637,21 +686,27 @@ class LocationTrackingService {
|
||||
Future<void> saveSettings() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
await prefs.setDouble(_prefKeyMinDistance, minDistanceMeters);
|
||||
await prefs.setDouble(_prefKeyMaxDistance, maxDistanceMeters);
|
||||
await prefs.setInt(_prefKeyMinTimeInterval, minTimeIntervalSeconds);
|
||||
await prefs.setDouble(_prefKeyGpsUpdateDistance, gpsUpdateDistance);
|
||||
await prefs.setBool(_prefKeyEnabled, isTracking);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyMinDistance), minDistanceMeters);
|
||||
await prefs.setDouble(_scopedKey(_prefKeyMaxDistance), maxDistanceMeters);
|
||||
await prefs.setInt(
|
||||
_scopedKey(_prefKeyMinTimeInterval),
|
||||
minTimeIntervalSeconds,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_scopedKey(_prefKeyGpsUpdateDistance),
|
||||
gpsUpdateDistance,
|
||||
);
|
||||
await prefs.setBool(_scopedKey(_prefKeyEnabled), isTracking);
|
||||
await prefs.setBool(
|
||||
_prefKeyFastLocationEnabled,
|
||||
_scopedKey(_prefKeyFastLocationEnabled),
|
||||
fastLocationUpdatesEnabled,
|
||||
);
|
||||
await prefs.setDouble(
|
||||
_prefKeyFastMovementThreshold,
|
||||
_scopedKey(_prefKeyFastMovementThreshold),
|
||||
fastLocationMovementThresholdMeters,
|
||||
);
|
||||
await prefs.setInt(
|
||||
_prefKeyFastActiveCadence,
|
||||
_scopedKey(_prefKeyFastActiveCadence),
|
||||
fastLocationActiveCadenceSeconds,
|
||||
);
|
||||
|
||||
|
||||
@@ -41,90 +41,103 @@ class MapMarkerService {
|
||||
Function(Contact)? onTap,
|
||||
double mapRotation = 0,
|
||||
Position? userPosition,
|
||||
Map<String, LatLng>? estimatedLocations,
|
||||
}) {
|
||||
return contacts.map((contact) {
|
||||
final location = contact.displayLocation;
|
||||
if (location == null) return null;
|
||||
return contacts
|
||||
.map((contact) {
|
||||
final location = contact.displayLocation ??
|
||||
estimatedLocations?[contact.publicKeyHex];
|
||||
if (location == null) return null;
|
||||
|
||||
return Marker(
|
||||
point: location,
|
||||
width: 80,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
child: Transform.rotate(
|
||||
angle: -mapRotation * pi / 180,
|
||||
child: GestureDetector(
|
||||
onTap: onTap != null ? () => onTap(contact) : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Location update time indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: getLocationAgeColor(contact),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.timeSinceLocationUpdate,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Marker icon
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BoxShape.rectangle
|
||||
: BoxShape.circle,
|
||||
borderRadius: contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BorderRadius.circular(14)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
return Marker(
|
||||
point: location,
|
||||
width: 80,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
child: Transform.rotate(
|
||||
angle: -mapRotation * pi / 180,
|
||||
child: GestureDetector(
|
||||
onTap: onTap != null ? () => onTap(contact) : null,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Location update time indicator
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getLocationAgeColor(contact),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.timeSinceLocationUpdate,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ContactAvatar(contact: contact, radius: 16),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Name label (without emoji)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.displayName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Marker icon
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape:
|
||||
contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BoxShape.rectangle
|
||||
: BoxShape.circle,
|
||||
borderRadius:
|
||||
contact.type == ContactType.channel ||
|
||||
contact.type == ContactType.room
|
||||
? BorderRadius.circular(14)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.3),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.all(2),
|
||||
child: ContactAvatar(contact: contact, radius: 16),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
// Name label (without emoji)
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 80),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
contact.displayName,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}).whereType<Marker>().toList();
|
||||
);
|
||||
})
|
||||
.whereType<Marker>()
|
||||
.toList();
|
||||
}
|
||||
|
||||
/// Generate markers for SAR events.
|
||||
@@ -141,10 +154,11 @@ class MapMarkerService {
|
||||
required BuildContext context,
|
||||
Function(SarMarker)? onTap,
|
||||
double mapRotation = 0,
|
||||
LatLng Function(LatLng point)? pointTransformer,
|
||||
}) {
|
||||
return sarMarkers.map((marker) {
|
||||
return Marker(
|
||||
point: marker.location,
|
||||
point: pointTransformer?.call(marker.location) ?? marker.location,
|
||||
width: 90,
|
||||
height: 100,
|
||||
rotate: false, // Don't rotate the entire marker with map
|
||||
@@ -157,7 +171,10 @@ class MapMarkerService {
|
||||
children: [
|
||||
// Time ago label
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: getSarMarkerColor(marker.type),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
@@ -188,7 +205,7 @@ class MapMarkerService {
|
||||
),
|
||||
padding: const EdgeInsets.all(6),
|
||||
child: Text(
|
||||
marker.emoji, // Use custom emoji if available
|
||||
marker.emoji, // Use custom emoji if available
|
||||
style: const TextStyle(fontSize: 18),
|
||||
),
|
||||
),
|
||||
@@ -196,13 +213,17 @@ class MapMarkerService {
|
||||
// Type label
|
||||
Container(
|
||||
constraints: const BoxConstraints(maxWidth: 90),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 4,
|
||||
vertical: 1,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.black.withValues(alpha: 0.7),
|
||||
borderRadius: BorderRadius.circular(3),
|
||||
),
|
||||
child: Text(
|
||||
marker.displayName, // Uses notes if available, otherwise type.displayName
|
||||
marker
|
||||
.displayName, // Uses notes if available, otherwise type.displayName
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 9,
|
||||
@@ -269,7 +290,8 @@ class MapMarkerService {
|
||||
final dLat = (lat2 - lat1) * pi / 180;
|
||||
final dLon = (lon2 - lon1) * pi / 180;
|
||||
|
||||
final a = sin(dLat / 2) * sin(dLat / 2) +
|
||||
final a =
|
||||
sin(dLat / 2) * sin(dLat / 2) +
|
||||
cos(lat1 * pi / 180) *
|
||||
cos(lat2 * pi / 180) *
|
||||
sin(dLon / 2) *
|
||||
@@ -299,8 +321,8 @@ class MapMarkerService {
|
||||
final lat2Rad = lat2 * pi / 180;
|
||||
|
||||
final y = sin(dLon) * cos(lat2Rad);
|
||||
final x = cos(lat1Rad) * sin(lat2Rad) -
|
||||
sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
||||
final x =
|
||||
cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
||||
|
||||
final bearing = atan2(y, x) * 180 / pi;
|
||||
return (bearing + 360) % 360;
|
||||
@@ -368,6 +390,8 @@ class MapMarkerService {
|
||||
return Colors.deepPurple; // Purple for repeaters
|
||||
case ContactType.room:
|
||||
return Colors.teal; // Teal for rooms
|
||||
case ContactType.sensor:
|
||||
return Colors.green; // Green for sensors
|
||||
case ContactType.channel:
|
||||
return Colors.orange; // Orange for channels
|
||||
case ContactType.none:
|
||||
@@ -389,6 +413,8 @@ class MapMarkerService {
|
||||
return Icons.router; // Router icon for repeaters
|
||||
case ContactType.room:
|
||||
return Icons.forum; // Forum/chat icon for rooms
|
||||
case ContactType.sensor:
|
||||
return Icons.sensors; // Sensors icon for sensor nodes
|
||||
case ContactType.channel:
|
||||
return Icons.public; // Public icon for channels
|
||||
case ContactType.none:
|
||||
@@ -466,7 +492,8 @@ class MapMarkerService {
|
||||
}
|
||||
|
||||
if (allPoints.isEmpty) {
|
||||
return defaultCenter ?? const LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||
return defaultCenter ??
|
||||
const LatLng(46.0569, 14.5058); // Ljubljana, Slovenia
|
||||
}
|
||||
|
||||
double lat = 0, lng = 0;
|
||||
|
||||
100
lib/services/map_workspace_snapshot_service.dart
Normal file
100
lib/services/map_workspace_snapshot_service.dart
Normal file
@@ -0,0 +1,100 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import '../providers/drawing_provider.dart';
|
||||
import '../providers/map_provider.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class MapWorkspaceSnapshotService {
|
||||
Future<MapWorkspaceProfileSection> capture({
|
||||
required MapProvider mapProvider,
|
||||
required DrawingProvider drawingProvider,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return MapWorkspaceProfileSection(
|
||||
mapPrefs: {
|
||||
'map_rotate_with_heading':
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('map_rotate_with_heading'),
|
||||
) ??
|
||||
false,
|
||||
'map_show_debug_info':
|
||||
prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('map_show_debug_info'),
|
||||
) ??
|
||||
false,
|
||||
'map_fullscreen':
|
||||
prefs.getBool(ProfileStorageScope.scopedKey('map_fullscreen')) ??
|
||||
false,
|
||||
'map_last_latitude': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_latitude'),
|
||||
),
|
||||
'map_last_longitude': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_longitude'),
|
||||
),
|
||||
'map_last_zoom': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_last_zoom'),
|
||||
),
|
||||
'map_last_layer_type': prefs.getInt(
|
||||
ProfileStorageScope.scopedKey('map_last_layer_type'),
|
||||
),
|
||||
'map_gps_update_distance': prefs.getDouble(
|
||||
ProfileStorageScope.scopedKey('map_gps_update_distance'),
|
||||
),
|
||||
'background_tracking_enabled': prefs.getBool(
|
||||
ProfileStorageScope.scopedKey('background_tracking_enabled'),
|
||||
),
|
||||
},
|
||||
drawings: drawingProvider.exportDrawingsJson(),
|
||||
currentTrail: mapProvider.currentTrail?.toJson(),
|
||||
trailHistory: mapProvider.trailHistory
|
||||
.map((trail) => trail.toJson())
|
||||
.toList(),
|
||||
importedTrail: mapProvider.importedTrail?.toJson(),
|
||||
isTrailVisible: mapProvider.isTrailVisible,
|
||||
showCadastralOverlay: mapProvider.showCadastralOverlay,
|
||||
showForestRoadsOverlay: mapProvider.showForestRoadsOverlay,
|
||||
showHikingTrailsOverlay: mapProvider.showHikingTrailsOverlay,
|
||||
showMainRoadsOverlay: mapProvider.showMainRoadsOverlay,
|
||||
showHouseNumbersOverlay: mapProvider.showHouseNumbersOverlay,
|
||||
showFireHazardZonesOverlay: mapProvider.showFireHazardZonesOverlay,
|
||||
showHistoricalFiresOverlay: mapProvider.showHistoricalFiresOverlay,
|
||||
showFirebreaksOverlay: mapProvider.showFirebreaksOverlay,
|
||||
showKrasFireZonesOverlay: mapProvider.showKrasFireZonesOverlay,
|
||||
showPlaceNamesOverlay: mapProvider.showPlaceNamesOverlay,
|
||||
showMunicipalityBordersOverlay:
|
||||
mapProvider.showMunicipalityBordersOverlay,
|
||||
showAllContactTrails: mapProvider.showAllContactTrails,
|
||||
hideRepeatersOnMap: mapProvider.hideRepeatersOnMap,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> apply(
|
||||
MapWorkspaceProfileSection? section, {
|
||||
required MapProvider mapProvider,
|
||||
required DrawingProvider drawingProvider,
|
||||
}) async {
|
||||
if (section == null || section.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final mapPrefs = section.mapPrefs ?? const <String, dynamic>{};
|
||||
for (final entry in mapPrefs.entries) {
|
||||
final key = ProfileStorageScope.scopedKey(entry.key);
|
||||
final value = entry.value;
|
||||
if (value == null) {
|
||||
await prefs.remove(key);
|
||||
} else if (value is bool) {
|
||||
await prefs.setBool(key, value);
|
||||
} else if (value is int) {
|
||||
await prefs.setInt(key, value);
|
||||
} else if (value is double) {
|
||||
await prefs.setDouble(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
mapProvider.applyWorkspaceJson(section.toJson());
|
||||
await drawingProvider.replaceDrawingsFromJson(section.drawings);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class MeshMapNode {
|
||||
final int type;
|
||||
@@ -18,6 +20,14 @@ class MeshMapNode {
|
||||
required this.updatedAtMs,
|
||||
});
|
||||
|
||||
bool get hasValidCoordinates =>
|
||||
latitude >= -90 &&
|
||||
latitude <= 90 &&
|
||||
longitude >= -180 &&
|
||||
longitude <= 180 &&
|
||||
latitude != 0 &&
|
||||
longitude != 0;
|
||||
|
||||
factory MeshMapNode.fromJson(Map<String, dynamic> json) {
|
||||
return MeshMapNode(
|
||||
type: (json['type'] as num?)?.toInt() ?? 0,
|
||||
@@ -33,25 +43,36 @@ class MeshMapNode {
|
||||
class MeshMapNodesService {
|
||||
static const String _nodesEndpoint =
|
||||
'https://api.meshcore.nz/api/v1/map/nodes';
|
||||
static const Duration _cacheTtl = Duration(minutes: 2);
|
||||
static const Duration traceCacheTtl = Duration(minutes: 10);
|
||||
static const int repeaterType = 1;
|
||||
static const Duration _cacheTtl = Duration(hours: 24);
|
||||
static const Duration traceCacheTtl = _cacheTtl;
|
||||
static const Duration traceTimeout = Duration(seconds: 30);
|
||||
static const String _cacheKey = 'mesh_map_nodes_cache_v1';
|
||||
static const String _cacheTimestampKey = 'mesh_map_nodes_cache_timestamp_v1';
|
||||
static List<MeshMapNode>? _cachedNodes;
|
||||
static DateTime? _cachedAt;
|
||||
static Future<void>? _ongoingSync;
|
||||
|
||||
static Future<List<MeshMapNode>> fetchNodes({
|
||||
bool forceRefresh = false,
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
bool allowNetwork = true,
|
||||
http.Client? client,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
if (!forceRefresh &&
|
||||
_cachedNodes != null &&
|
||||
cached.isNotEmpty &&
|
||||
_cachedAt != null &&
|
||||
now.difference(_cachedAt!) < cacheTtl) {
|
||||
return _cachedNodes!;
|
||||
return cached;
|
||||
}
|
||||
|
||||
final response = await http
|
||||
if (!allowNetwork) {
|
||||
return cached;
|
||||
}
|
||||
|
||||
final response = await (client ?? http.Client())
|
||||
.get(Uri.parse(_nodesEndpoint))
|
||||
.timeout(traceTimeout);
|
||||
if (response.statusCode < 200 || response.statusCode >= 300) {
|
||||
@@ -64,13 +85,134 @@ class MeshMapNodesService {
|
||||
final nodes = nodesRaw
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(MeshMapNode.fromJson)
|
||||
.where(
|
||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
||||
)
|
||||
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||
.toList();
|
||||
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = now;
|
||||
await _storeCache(nodes, cachedAt: now);
|
||||
return nodes;
|
||||
}
|
||||
|
||||
static Future<List<MeshMapNode>> loadCachedNodes({
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
if (_cachedNodes != null &&
|
||||
_cachedAt != null &&
|
||||
now.difference(_cachedAt!) < cacheTtl) {
|
||||
return _cachedNodes!;
|
||||
}
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||
final cachedJson = prefs.getString(_cacheKey);
|
||||
if (cachedAtMs == null || cachedJson == null) {
|
||||
_cachedNodes = null;
|
||||
_cachedAt = null;
|
||||
return const [];
|
||||
}
|
||||
|
||||
final cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||
if (now.difference(cachedAt) >= cacheTtl) {
|
||||
_cachedNodes = null;
|
||||
_cachedAt = cachedAt;
|
||||
return const [];
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(cachedJson) as List<dynamic>;
|
||||
final nodes = decoded
|
||||
.whereType<Map<String, dynamic>>()
|
||||
.map(MeshMapNode.fromJson)
|
||||
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||
.toList();
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = cachedAt;
|
||||
return nodes;
|
||||
}
|
||||
|
||||
static Future<bool> hasFreshCache({Duration cacheTtl = _cacheTtl}) async {
|
||||
final nodes = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
return nodes.isNotEmpty;
|
||||
}
|
||||
|
||||
static Future<void> syncInBackgroundIfStale({
|
||||
Duration cacheTtl = _cacheTtl,
|
||||
http.Client? client,
|
||||
}) async {
|
||||
final now = DateTime.now();
|
||||
if (_cachedAt != null && now.difference(_cachedAt!) < cacheTtl) {
|
||||
return;
|
||||
}
|
||||
|
||||
final cached = await loadCachedNodes(cacheTtl: cacheTtl);
|
||||
if (cached.isNotEmpty && _cachedAt != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (_ongoingSync != null) {
|
||||
return _ongoingSync!;
|
||||
}
|
||||
|
||||
_ongoingSync = () async {
|
||||
try {
|
||||
await fetchNodes(
|
||||
forceRefresh: true,
|
||||
cacheTtl: cacheTtl,
|
||||
allowNetwork: true,
|
||||
client: client,
|
||||
);
|
||||
} catch (_) {
|
||||
// Background refresh is best-effort only.
|
||||
} finally {
|
||||
_ongoingSync = null;
|
||||
}
|
||||
}();
|
||||
|
||||
return _ongoingSync!;
|
||||
}
|
||||
|
||||
static Future<void> clearCache() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_cacheKey);
|
||||
await prefs.remove(_cacheTimestampKey);
|
||||
_cachedNodes = null;
|
||||
_cachedAt = null;
|
||||
}
|
||||
|
||||
static Future<DateTime?> cachedAt() async {
|
||||
if (_cachedAt != null) return _cachedAt;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final cachedAtMs = prefs.getInt(_cacheTimestampKey);
|
||||
if (cachedAtMs == null) return null;
|
||||
_cachedAt = DateTime.fromMillisecondsSinceEpoch(cachedAtMs);
|
||||
return _cachedAt;
|
||||
}
|
||||
|
||||
static bool isRepeater(MeshMapNode node) => node.type == repeaterType;
|
||||
|
||||
static Future<void> _storeCache(
|
||||
List<MeshMapNode> nodes, {
|
||||
required DateTime cachedAt,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_cacheKey,
|
||||
jsonEncode(
|
||||
nodes
|
||||
.map(
|
||||
(node) => {
|
||||
'type': node.type,
|
||||
'name': node.name,
|
||||
'public_key': node.publicKey,
|
||||
'latitude': node.latitude,
|
||||
'longitude': node.longitude,
|
||||
'updated_at': node.updatedAtMs,
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
await prefs.setInt(_cacheTimestampKey, cachedAt.millisecondsSinceEpoch);
|
||||
_cachedNodes = nodes;
|
||||
_cachedAt = cachedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,13 @@ import '../models/message_contact_location.dart';
|
||||
import '../models/message_reception_details.dart';
|
||||
import '../models/message_transfer_details.dart';
|
||||
import '../models/message_route_metadata.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
|
||||
/// Service for persisting messages to local storage
|
||||
class MessageStorageService {
|
||||
static const String _messagesKey = 'stored_messages';
|
||||
static const String _removedSarMarkerIdsKey = 'removed_sar_marker_ids';
|
||||
static const String _messageContactLocationsKey =
|
||||
'stored_message_contact_locations';
|
||||
static const String _messageReceptionDetailsKey =
|
||||
@@ -19,8 +21,14 @@ class MessageStorageService {
|
||||
'stored_message_transfer_details';
|
||||
static const String _messageRouteMetadataKey =
|
||||
'stored_message_route_metadata';
|
||||
static const String _embeddedReceptionDetailsKey = 'storedReceptionDetails';
|
||||
static const String _legacyPathBytesKey = 'storedPathBytes';
|
||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
||||
|
||||
String _key(String baseKey, {String? namespace}) {
|
||||
return ProfileStorageScope.scopedKey(baseKey, namespace: namespace);
|
||||
}
|
||||
|
||||
/// Save messages to persistent storage
|
||||
Future<void> saveMessages(
|
||||
List<Message> messages, {
|
||||
@@ -28,12 +36,21 @@ class MessageStorageService {
|
||||
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
||||
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
||||
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// Convert messages to JSON
|
||||
final jsonList = messages.map((msg) => _messageToJson(msg)).toList();
|
||||
// Convert messages to JSON and embed path bytes as a fallback so they
|
||||
// survive restore even if the sidecar reception-details entry is absent.
|
||||
final jsonList = messages
|
||||
.map(
|
||||
(msg) => _messageToJson(
|
||||
msg,
|
||||
receptionDetails: messageReceptionDetails[msg.id],
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
// Limit to max stored messages (keep most recent)
|
||||
final limitedList = jsonList.length > _maxStoredMessages
|
||||
@@ -41,7 +58,10 @@ class MessageStorageService {
|
||||
: jsonList;
|
||||
|
||||
final jsonString = jsonEncode(limitedList);
|
||||
await prefs.setString(_messagesKey, jsonString);
|
||||
await prefs.setString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
jsonString,
|
||||
);
|
||||
final retainedMessageIds = limitedList
|
||||
.map((entry) => entry['id'] as String)
|
||||
.toSet();
|
||||
@@ -70,19 +90,19 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
await prefs.setString(
|
||||
_messageContactLocationsKey,
|
||||
_key(_messageContactLocationsKey, namespace: namespace),
|
||||
jsonEncode(locationJson),
|
||||
);
|
||||
await prefs.setString(
|
||||
_messageReceptionDetailsKey,
|
||||
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||
jsonEncode(receptionJson),
|
||||
);
|
||||
await prefs.setString(
|
||||
_messageTransferDetailsKey,
|
||||
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||
jsonEncode(transferJson),
|
||||
);
|
||||
await prefs.setString(
|
||||
_messageRouteMetadataKey,
|
||||
_key(_messageRouteMetadataKey, namespace: namespace),
|
||||
jsonEncode(routeMetadataJson),
|
||||
);
|
||||
|
||||
@@ -94,11 +114,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageContactLocation>>
|
||||
loadMessageContactLocations() async {
|
||||
Future<Map<String, MessageContactLocation>> loadMessageContactLocations({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageContactLocationsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageContactLocationsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -124,29 +147,48 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageReceptionDetails>>
|
||||
loadMessageReceptionDetails() async {
|
||||
Future<Map<String, MessageReceptionDetails>> loadMessageReceptionDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageReceptionDetailsKey);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! Map<String, dynamic>) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||
);
|
||||
final result = <String, MessageReceptionDetails>{};
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
final snapshot = MessageReceptionDetails.fromJson(value);
|
||||
if (snapshot != null) {
|
||||
result[entry.key] = snapshot;
|
||||
if (jsonString != null && jsonString.isNotEmpty) {
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
for (final entry in decoded.entries) {
|
||||
final value = entry.value;
|
||||
if (value is! Map<String, dynamic>) continue;
|
||||
final snapshot = MessageReceptionDetails.fromJson(value);
|
||||
if (snapshot != null) {
|
||||
result[entry.key] = snapshot;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final embeddedReceptionDetails = await _loadEmbeddedReceptionDetails(
|
||||
namespace: namespace,
|
||||
);
|
||||
embeddedReceptionDetails.forEach((messageId, snapshot) {
|
||||
result.putIfAbsent(messageId, () => snapshot);
|
||||
});
|
||||
|
||||
final fallbackPathBytes = await _loadLegacyPathBytesFromMessages(
|
||||
namespace: namespace,
|
||||
);
|
||||
fallbackPathBytes.forEach((messageId, pathBytes) {
|
||||
result.putIfAbsent(
|
||||
messageId,
|
||||
() => MessageReceptionDetails(
|
||||
capturedAt: DateTime.fromMillisecondsSinceEpoch(0),
|
||||
pathBytes: pathBytes,
|
||||
),
|
||||
);
|
||||
});
|
||||
return result;
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error loading reception details: $e');
|
||||
@@ -154,11 +196,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageTransferDetails>>
|
||||
loadMessageTransferDetails() async {
|
||||
Future<Map<String, MessageTransferDetails>> loadMessageTransferDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageTransferDetailsKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -184,10 +229,14 @@ class MessageStorageService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata() async {
|
||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata({
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messageRouteMetadataKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messageRouteMetadataKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
@@ -211,10 +260,12 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Load messages from persistent storage
|
||||
Future<List<Message>> loadMessages() async {
|
||||
Future<List<Message>> loadMessages({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
debugPrint('ℹ️ [MessageStorage] No stored messages found');
|
||||
@@ -239,25 +290,67 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Clear all stored messages
|
||||
Future<void> clearMessages() async {
|
||||
Future<void> clearMessages({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_messagesKey);
|
||||
await prefs.remove(_messageContactLocationsKey);
|
||||
await prefs.remove(_messageReceptionDetailsKey);
|
||||
await prefs.remove(_messageTransferDetailsKey);
|
||||
await prefs.remove(_messageRouteMetadataKey);
|
||||
await prefs.remove(_key(_messagesKey, namespace: namespace));
|
||||
await prefs.remove(
|
||||
_key(_messageContactLocationsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(
|
||||
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(
|
||||
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||
);
|
||||
await prefs.remove(_key(_messageRouteMetadataKey, namespace: namespace));
|
||||
await prefs.remove(_key(_removedSarMarkerIdsKey, namespace: namespace));
|
||||
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error clearing messages: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats() async {
|
||||
Future<Set<String>> loadRemovedSarMarkerIds({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(_messagesKey);
|
||||
final ids =
|
||||
prefs.getStringList(
|
||||
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||
) ??
|
||||
const [];
|
||||
return ids.toSet();
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error loading removed SAR marker IDs: $e');
|
||||
return const <String>{};
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveRemovedSarMarkerIds(
|
||||
Set<String> ids, {
|
||||
String? namespace,
|
||||
}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setStringList(
|
||||
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||
ids.toList()..sort(),
|
||||
);
|
||||
debugPrint(
|
||||
'✅ [MessageStorage] Saved ${ids.length} removed SAR marker IDs',
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [MessageStorage] Error saving removed SAR marker IDs: $e');
|
||||
}
|
||||
}
|
||||
|
||||
/// Get storage statistics
|
||||
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||
@@ -278,7 +371,12 @@ class MessageStorageService {
|
||||
}
|
||||
|
||||
/// Convert Message to JSON
|
||||
Map<String, dynamic> _messageToJson(Message message) {
|
||||
Map<String, dynamic> _messageToJson(
|
||||
Message message, {
|
||||
MessageReceptionDetails? receptionDetails,
|
||||
}) {
|
||||
final persistedPathBytes =
|
||||
message.pathBytes ?? _pathBytesFromSnapshot(receptionDetails);
|
||||
return {
|
||||
'id': message.id,
|
||||
'messageType': message.messageType.name,
|
||||
@@ -287,12 +385,16 @@ class MessageStorageService {
|
||||
: null,
|
||||
'channelIdx': message.channelIdx,
|
||||
'pathLen': message.pathLen,
|
||||
'pathBytes': persistedPathBytes?.toList(),
|
||||
'textType': message.textType.value,
|
||||
'senderTimestamp': message.senderTimestamp,
|
||||
'text': message.text,
|
||||
'isSarMarker': message.isSarMarker,
|
||||
'sarGpsLat': message.sarGpsCoordinates?.latitude,
|
||||
'sarGpsLon': message.sarGpsCoordinates?.longitude,
|
||||
'sarCustomMapLat': message.sarCustomMapPoint?.latitude,
|
||||
'sarCustomMapLon': message.sarCustomMapPoint?.longitude,
|
||||
'sarCustomMapId': message.sarCustomMapId,
|
||||
'sarNotes': message.sarNotes,
|
||||
'sarCustomEmoji': message.sarCustomEmoji,
|
||||
'sarColorIndex': message.sarColorIndex,
|
||||
@@ -338,9 +440,74 @@ class MessageStorageService {
|
||||
},
|
||||
)
|
||||
.toList(),
|
||||
if (receptionDetails != null)
|
||||
_embeddedReceptionDetailsKey: receptionDetails.toJson(),
|
||||
if (receptionDetails?.pathBytes case final pathBytes?)
|
||||
_legacyPathBytesKey: List<int>.from(pathBytes),
|
||||
};
|
||||
}
|
||||
|
||||
Future<Map<String, MessageReceptionDetails>> _loadEmbeddedReceptionDetails({
|
||||
String? namespace,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! List) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final result = <String, MessageReceptionDetails>{};
|
||||
for (final entry in decoded) {
|
||||
if (entry is! Map<String, dynamic>) continue;
|
||||
final messageId = entry['id'];
|
||||
final embedded = entry[_embeddedReceptionDetailsKey];
|
||||
if (messageId is! String || embedded is! Map<String, dynamic>) continue;
|
||||
final snapshot = MessageReceptionDetails.fromJson(embedded);
|
||||
if (snapshot == null) continue;
|
||||
result[messageId] = snapshot;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Future<Map<String, List<int>>> _loadLegacyPathBytesFromMessages({
|
||||
String? namespace,
|
||||
}) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final jsonString = prefs.getString(
|
||||
_key(_messagesKey, namespace: namespace),
|
||||
);
|
||||
if (jsonString == null || jsonString.isEmpty) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final decoded = jsonDecode(jsonString);
|
||||
if (decoded is! List) {
|
||||
return const {};
|
||||
}
|
||||
|
||||
final result = <String, List<int>>{};
|
||||
for (final entry in decoded) {
|
||||
if (entry is! Map<String, dynamic>) continue;
|
||||
final messageId = entry['id'];
|
||||
final pathBytes = entry[_legacyPathBytesKey];
|
||||
if (messageId is! String || pathBytes is! List) continue;
|
||||
final normalized = pathBytes
|
||||
.whereType<num>()
|
||||
.map((b) => b.toInt())
|
||||
.toList();
|
||||
if (normalized.isEmpty) continue;
|
||||
result[messageId] = normalized;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Convert JSON to Message
|
||||
Message? _messageFromJson(Map<String, dynamic> json) {
|
||||
try {
|
||||
@@ -357,14 +524,33 @@ class MessageStorageService {
|
||||
: null,
|
||||
channelIdx: json['channelIdx'] as int?,
|
||||
pathLen: json['pathLen'] as int,
|
||||
pathBytes: json['pathBytes'] is List
|
||||
? Uint8List.fromList(
|
||||
(json['pathBytes'] as List<dynamic>)
|
||||
.whereType<num>()
|
||||
.map((b) => b.toInt())
|
||||
.toList(),
|
||||
)
|
||||
: null,
|
||||
textType: MessageTextType.fromValue(json['textType'] as int),
|
||||
senderTimestamp: json['senderTimestamp'] as int,
|
||||
text: json['text'] as String,
|
||||
isSarMarker: json['isSarMarker'] as bool? ?? false,
|
||||
sarGpsCoordinates:
|
||||
json['sarGpsLat'] != null && json['sarGpsLon'] != null
|
||||
? LatLng(json['sarGpsLat'] as double, json['sarGpsLon'] as double)
|
||||
? LatLng(
|
||||
(json['sarGpsLat'] as num).toDouble(),
|
||||
(json['sarGpsLon'] as num).toDouble(),
|
||||
)
|
||||
: null,
|
||||
sarCustomMapPoint:
|
||||
json['sarCustomMapLat'] != null && json['sarCustomMapLon'] != null
|
||||
? LatLng(
|
||||
(json['sarCustomMapLat'] as num).toDouble(),
|
||||
(json['sarCustomMapLon'] as num).toDouble(),
|
||||
)
|
||||
: null,
|
||||
sarCustomMapId: json['sarCustomMapId'] as String?,
|
||||
sarNotes: json['sarNotes'] as String?,
|
||||
sarCustomEmoji: json['sarCustomEmoji'] as String?,
|
||||
sarColorIndex: json['sarColorIndex'] as int?,
|
||||
@@ -454,4 +640,12 @@ class MessageStorageService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Uint8List? _pathBytesFromSnapshot(MessageReceptionDetails? receptionDetails) {
|
||||
final pathBytes = receptionDetails?.pathBytes;
|
||||
if (pathBytes == null || pathBytes.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
return Uint8List.fromList(pathBytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class MessagingRoutePreferences {
|
||||
static const bool defaultAutoRouteRotationEnabled = false;
|
||||
@@ -14,33 +15,49 @@ class MessagingRoutePreferences {
|
||||
|
||||
static Future<bool> getAutoRouteRotationEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_autoRouteRotationKey) ??
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||
) ??
|
||||
defaultAutoRouteRotationEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setAutoRouteRotationEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_autoRouteRotationKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getClearPathOnMaxRetry() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_clearPathOnMaxRetryKey) ?? defaultClearPathOnMaxRetry;
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||
) ??
|
||||
defaultClearPathOnMaxRetry;
|
||||
}
|
||||
|
||||
static Future<void> setClearPathOnMaxRetry(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
|
||||
static Future<bool> getNearestRelayFallbackEnabled() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getBool(_nearestRelayFallbackKey) ??
|
||||
return prefs.getBool(
|
||||
ProfileStorageScope.scopedKey(_nearestRelayFallbackKey),
|
||||
) ??
|
||||
defaultNearestRelayFallbackEnabled;
|
||||
}
|
||||
|
||||
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_nearestRelayFallbackKey, enabled);
|
||||
await prefs.setBool(
|
||||
ProfileStorageScope.scopedKey(_nearestRelayFallbackKey),
|
||||
enabled,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:timezone/data/latest_all.dart' as tz;
|
||||
import '../models/sar_marker.dart';
|
||||
import '../l10n/app_localizations.dart';
|
||||
@@ -13,14 +14,28 @@ class NotificationService {
|
||||
|
||||
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||
FlutterLocalNotificationsPlugin();
|
||||
static const String _prefMessagesEnabled = 'notifications_messages_enabled';
|
||||
static const String _prefSarEnabled = 'notifications_sar_enabled';
|
||||
static const String _prefDiscoveryEnabled = 'notifications_discovery_enabled';
|
||||
static const String _prefUpdatesEnabled = 'notifications_updates_enabled';
|
||||
static const String _prefMuteForeground = 'notifications_mute_foreground';
|
||||
|
||||
bool _isInitialized = false;
|
||||
bool _permissionGranted = false;
|
||||
bool _messageNotificationsEnabled = true;
|
||||
bool _sarNotificationsEnabled = true;
|
||||
bool _discoveryNotificationsEnabled = true;
|
||||
bool _updateNotificationsEnabled = true;
|
||||
bool _muteForegroundNotifications = true;
|
||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||
String? _launchPayload;
|
||||
|
||||
// Notification IDs
|
||||
static const int _sarNotificationId = 1000;
|
||||
static const int _messageNotificationId = 2000;
|
||||
static const int _updateNotificationId = 3000;
|
||||
static const int _batteryNotificationId = 4000;
|
||||
static const int _discoveryNotificationId = 5000;
|
||||
|
||||
// Notification channels
|
||||
static const String _urgentChannelId = 'sar_urgent';
|
||||
@@ -38,12 +53,32 @@ class NotificationService {
|
||||
static const String _updateChannelDescription =
|
||||
'Notifications for available app updates';
|
||||
|
||||
static const String _batteryChannelId = 'battery_alerts';
|
||||
static const String _batteryChannelName = 'Battery Alerts';
|
||||
static const String _batteryChannelDescription =
|
||||
'Notifications when a device or contact battery is low';
|
||||
|
||||
static const String _discoveryChannelId = 'discovery_alerts';
|
||||
static const String _discoveryChannelName = 'Discovery Alerts';
|
||||
static const String _discoveryChannelDescription =
|
||||
'Notifications when new contacts are discovered';
|
||||
|
||||
bool get messageNotificationsEnabled => _messageNotificationsEnabled;
|
||||
bool get sarNotificationsEnabled => _sarNotificationsEnabled;
|
||||
bool get discoveryNotificationsEnabled => _discoveryNotificationsEnabled;
|
||||
bool get updateNotificationsEnabled => _updateNotificationsEnabled;
|
||||
bool get muteForegroundNotifications => _muteForegroundNotifications;
|
||||
bool get isAppInForeground => _lifecycleState == AppLifecycleState.resumed;
|
||||
|
||||
/// Initialize notification service
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
|
||||
try {
|
||||
debugPrint('📬 [NotificationService] Initializing...');
|
||||
WidgetsBinding.instance.addObserver(_LifecycleObserver(this));
|
||||
_lifecycleState =
|
||||
WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed;
|
||||
|
||||
// Initialize timezone data
|
||||
tz.initializeTimeZones();
|
||||
@@ -72,8 +107,15 @@ class NotificationService {
|
||||
onDidReceiveNotificationResponse: _onNotificationResponse,
|
||||
);
|
||||
|
||||
final launchDetails = await _notificationsPlugin
|
||||
.getNotificationAppLaunchDetails();
|
||||
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
||||
_launchPayload = launchDetails?.notificationResponse?.payload;
|
||||
}
|
||||
|
||||
// Request permissions
|
||||
await _requestPermissions();
|
||||
await _loadPreferences();
|
||||
|
||||
// Create notification channels (Android)
|
||||
await _createNotificationChannels();
|
||||
@@ -132,6 +174,54 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadPreferences() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_messageNotificationsEnabled = prefs.getBool(_prefMessagesEnabled) ?? true;
|
||||
_sarNotificationsEnabled = prefs.getBool(_prefSarEnabled) ?? true;
|
||||
_discoveryNotificationsEnabled =
|
||||
prefs.getBool(_prefDiscoveryEnabled) ?? true;
|
||||
_updateNotificationsEnabled = prefs.getBool(_prefUpdatesEnabled) ?? true;
|
||||
_muteForegroundNotifications = prefs.getBool(_prefMuteForeground) ?? true;
|
||||
}
|
||||
|
||||
Future<void> setMessageNotificationsEnabled(bool value) async {
|
||||
_messageNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefMessagesEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setSarNotificationsEnabled(bool value) async {
|
||||
_sarNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefSarEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setDiscoveryNotificationsEnabled(bool value) async {
|
||||
_discoveryNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefDiscoveryEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setUpdateNotificationsEnabled(bool value) async {
|
||||
_updateNotificationsEnabled = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefUpdatesEnabled, value);
|
||||
}
|
||||
|
||||
Future<void> setMuteForegroundNotifications(bool value) async {
|
||||
_muteForegroundNotifications = value;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setBool(_prefMuteForeground, value);
|
||||
}
|
||||
|
||||
void handleAppLifecycleStateChanged(AppLifecycleState state) {
|
||||
_lifecycleState = state;
|
||||
}
|
||||
|
||||
bool _shouldSuppressForegroundNotifications() {
|
||||
return _muteForegroundNotifications && isAppInForeground;
|
||||
}
|
||||
|
||||
/// Create notification channels for Android
|
||||
Future<void> _createNotificationChannels() async {
|
||||
try {
|
||||
@@ -177,9 +267,31 @@ class NotificationService {
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
const batteryChannel = AndroidNotificationChannel(
|
||||
_batteryChannelId,
|
||||
_batteryChannelName,
|
||||
description: _batteryChannelDescription,
|
||||
importance: Importance.high,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
const discoveryChannel = AndroidNotificationChannel(
|
||||
_discoveryChannelId,
|
||||
_discoveryChannelName,
|
||||
description: _discoveryChannelDescription,
|
||||
importance: Importance.high,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showBadge: true,
|
||||
);
|
||||
|
||||
await androidPlugin.createNotificationChannel(urgentChannel);
|
||||
await androidPlugin.createNotificationChannel(messagesChannel);
|
||||
await androidPlugin.createNotificationChannel(updateChannel);
|
||||
await androidPlugin.createNotificationChannel(batteryChannel);
|
||||
await androidPlugin.createNotificationChannel(discoveryChannel);
|
||||
debugPrint('✅ [NotificationService] Created notification channels');
|
||||
} catch (e) {
|
||||
debugPrint('⚠️ [NotificationService] Error creating channels: $e');
|
||||
@@ -201,6 +313,12 @@ class NotificationService {
|
||||
}
|
||||
}
|
||||
|
||||
String? consumeLaunchPayload() {
|
||||
final payload = _launchPayload;
|
||||
_launchPayload = null;
|
||||
return payload;
|
||||
}
|
||||
|
||||
/// Show urgent notification for SAR marker
|
||||
Future<void> showSarNotification({
|
||||
required SarMarkerType type,
|
||||
@@ -222,6 +340,16 @@ class NotificationService {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_sarNotificationsEnabled) {
|
||||
debugPrint('ℹ️ [NotificationService] SAR notifications disabled');
|
||||
return;
|
||||
}
|
||||
if (_shouldSuppressForegroundNotifications()) {
|
||||
debugPrint(
|
||||
'ℹ️ [NotificationService] App in foreground, skipping SAR notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique notification ID based on timestamp
|
||||
@@ -394,6 +522,16 @@ class NotificationService {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_messageNotificationsEnabled) {
|
||||
debugPrint('ℹ️ [NotificationService] Message notifications disabled');
|
||||
return;
|
||||
}
|
||||
if (_shouldSuppressForegroundNotifications()) {
|
||||
debugPrint(
|
||||
'ℹ️ [NotificationService] App in foreground, skipping message notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Generate unique notification ID based on timestamp
|
||||
@@ -402,10 +540,13 @@ class NotificationService {
|
||||
(DateTime.now().millisecondsSinceEpoch % 1000);
|
||||
|
||||
// Build notification title and body
|
||||
final resolvedChannelName = channelName?.trim().isNotEmpty == true
|
||||
? channelName!.trim()
|
||||
: (localizations?.publicChannel ?? 'Public');
|
||||
final title = isChannelMessage
|
||||
? (localizations != null
|
||||
? '${localizations.channel}: ${channelName ?? "Public"}'
|
||||
: 'Channel: ${channelName ?? "Public"}')
|
||||
? '${localizations.channel}: $resolvedChannelName'
|
||||
: 'Channel: $resolvedChannelName')
|
||||
: (localizations != null
|
||||
? '${localizations.newMessage} ${localizations.from} $senderName'
|
||||
: 'New message from $senderName');
|
||||
@@ -544,13 +685,23 @@ class NotificationService {
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!_updateNotificationsEnabled) {
|
||||
debugPrint('ℹ️ [NotificationService] Update notifications disabled');
|
||||
return;
|
||||
}
|
||||
if (_shouldSuppressForegroundNotifications()) {
|
||||
debugPrint(
|
||||
'ℹ️ [NotificationService] App in foreground, skipping update notification',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Build notification title and body
|
||||
final title = localizations?.updateAvailable ?? 'App Update Available';
|
||||
final body = localizations != null
|
||||
? '${localizations.currentVersion}: $currentVersion\n'
|
||||
'${localizations.latestVersion}: $latestVersion'
|
||||
'${localizations.latestVersion}: $latestVersion'
|
||||
: 'Current: $currentVersion\nLatest: $latestVersion';
|
||||
|
||||
// Android notification details
|
||||
@@ -614,4 +765,169 @@ class NotificationService {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> showLowBatteryNotification({
|
||||
required String nodeId,
|
||||
required String nodeName,
|
||||
required double batteryPercent,
|
||||
required bool isCurrentDevice,
|
||||
}) async {
|
||||
if (!_isInitialized) {
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Not initialized, skipping notification',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_permissionGranted) {
|
||||
debugPrint(
|
||||
'⚠️ [NotificationService] Permission not granted, skipping notification',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!_messageNotificationsEnabled) {
|
||||
debugPrint('ℹ️ [NotificationService] Message notifications disabled');
|
||||
return false;
|
||||
}
|
||||
if (_shouldSuppressForegroundNotifications()) {
|
||||
debugPrint(
|
||||
'ℹ️ [NotificationService] App in foreground, skipping low battery notification',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
final roundedPercent = batteryPercent.round().clamp(0, 100);
|
||||
final title = isCurrentDevice
|
||||
? 'Device battery low'
|
||||
: 'Contact battery low';
|
||||
final body = isCurrentDevice
|
||||
? '$nodeName battery is at $roundedPercent%.'
|
||||
: '$nodeName is at $roundedPercent% battery.';
|
||||
final notificationId =
|
||||
_batteryNotificationId + ((nodeId.hashCode & 0x7fffffff) % 1000);
|
||||
|
||||
try {
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_batteryChannelId,
|
||||
_batteryChannelName,
|
||||
channelDescription: _batteryChannelDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
ticker: title,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showWhen: true,
|
||||
when: DateTime.now().millisecondsSinceEpoch,
|
||||
styleInformation: BigTextStyleInformation(
|
||||
body,
|
||||
contentTitle: title,
|
||||
summaryText: '$roundedPercent%',
|
||||
),
|
||||
);
|
||||
|
||||
final darwinDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
sound: 'default',
|
||||
threadIdentifier: 'battery_alerts',
|
||||
subtitle: '$roundedPercent%',
|
||||
);
|
||||
|
||||
final notificationDetails = NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: darwinDetails,
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id: notificationId,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: notificationDetails,
|
||||
payload: 'battery:$nodeId',
|
||||
);
|
||||
|
||||
debugPrint(
|
||||
'✅ [NotificationService] Showed low battery notification for $nodeName ($roundedPercent%)',
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'❌ [NotificationService] Error showing low battery notification: $e',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> showContactDiscoveredNotification({
|
||||
required String contactKey,
|
||||
String? contactName,
|
||||
}) async {
|
||||
if (!_isInitialized) return false;
|
||||
if (!_permissionGranted) return false;
|
||||
if (!_discoveryNotificationsEnabled) return false;
|
||||
if (_shouldSuppressForegroundNotifications()) return false;
|
||||
|
||||
final shortKey = contactKey.length > 12
|
||||
? contactKey.substring(0, 12).toUpperCase()
|
||||
: contactKey.toUpperCase();
|
||||
final title = 'New contact discovered';
|
||||
final resolvedName = contactName?.trim();
|
||||
final body = resolvedName != null && resolvedName.isNotEmpty
|
||||
? '$resolvedName is available in Discovery.'
|
||||
: 'New contact $shortKey is available in Discovery.';
|
||||
final notificationId =
|
||||
_discoveryNotificationId + ((contactKey.hashCode & 0x7fffffff) % 1000);
|
||||
|
||||
try {
|
||||
final androidDetails = AndroidNotificationDetails(
|
||||
_discoveryChannelId,
|
||||
_discoveryChannelName,
|
||||
channelDescription: _discoveryChannelDescription,
|
||||
importance: Importance.high,
|
||||
priority: Priority.high,
|
||||
ticker: title,
|
||||
playSound: true,
|
||||
enableVibration: true,
|
||||
showWhen: true,
|
||||
when: DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
|
||||
final darwinDetails = DarwinNotificationDetails(
|
||||
presentAlert: true,
|
||||
presentBadge: true,
|
||||
presentSound: true,
|
||||
sound: 'default',
|
||||
threadIdentifier: 'discovery_alerts',
|
||||
);
|
||||
|
||||
await _notificationsPlugin.show(
|
||||
id: notificationId,
|
||||
title: title,
|
||||
body: body,
|
||||
notificationDetails: NotificationDetails(
|
||||
android: androidDetails,
|
||||
iOS: darwinDetails,
|
||||
),
|
||||
payload: 'discovery:$contactKey',
|
||||
);
|
||||
return true;
|
||||
} catch (e) {
|
||||
debugPrint(
|
||||
'❌ [NotificationService] Error showing discovery notification: $e',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _LifecycleObserver with WidgetsBindingObserver {
|
||||
_LifecycleObserver(this._service);
|
||||
|
||||
final NotificationService _service;
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
_service.handleAppLifecycleStateChanged(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,6 +61,9 @@ class PacketCaptureStorageService {
|
||||
File? _file;
|
||||
|
||||
Future<File> _resolveFile() async {
|
||||
if (kIsWeb) {
|
||||
throw UnsupportedError('Packet capture file storage is not supported on web');
|
||||
}
|
||||
if (_file != null) return _file!;
|
||||
final dir = await getApplicationSupportDirectory();
|
||||
final file = File('${dir.path}/$_fileName');
|
||||
@@ -73,6 +76,7 @@ class PacketCaptureStorageService {
|
||||
|
||||
Future<void> appendLogs(List<BlePacketLog> logs) async {
|
||||
if (logs.isEmpty) return;
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
final sink = file.openWrite(mode: FileMode.append);
|
||||
@@ -98,6 +102,7 @@ class PacketCaptureStorageService {
|
||||
}
|
||||
|
||||
Future<List<StoredPacketCapture>> loadRecent({int limit = 500}) async {
|
||||
if (kIsWeb) return const [];
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (!await file.exists()) return const [];
|
||||
@@ -116,6 +121,7 @@ class PacketCaptureStorageService {
|
||||
}
|
||||
|
||||
Future<int> count() async {
|
||||
if (kIsWeb) return 0;
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (!await file.exists()) return 0;
|
||||
@@ -127,6 +133,7 @@ class PacketCaptureStorageService {
|
||||
}
|
||||
|
||||
Future<void> clear() async {
|
||||
if (kIsWeb) return;
|
||||
try {
|
||||
final file = await _resolveFile();
|
||||
if (await file.exists()) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../models/contact.dart';
|
||||
import '../models/path_history.dart';
|
||||
import '../models/path_selection.dart';
|
||||
import '../utils/log_rx_route_decoder.dart';
|
||||
|
||||
class PathHistoryService {
|
||||
static const String _storageKey = 'contact_path_history_v1';
|
||||
@@ -53,6 +54,7 @@ class PathHistoryService {
|
||||
pathBytes: contact.routePathBytes.toList(),
|
||||
hopCount: contact.routeHopCount,
|
||||
hashSize: contact.routeHashSize,
|
||||
source: existing?.source ?? PathRecordSource.learned,
|
||||
successCount: existing?.successCount ?? 0,
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
@@ -83,15 +85,21 @@ class PathHistoryService {
|
||||
return;
|
||||
}
|
||||
|
||||
final normalizedPathBytes = LogRxRouteDecoder.reverseHopBytes(
|
||||
pathBytes,
|
||||
hashSize: hashSize,
|
||||
);
|
||||
|
||||
final history = _historyFor(contactPublicKeyHex);
|
||||
final signature = pathBytes
|
||||
final signature = normalizedPathBytes
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
final existing = _findDirectPath(history.directPaths, signature);
|
||||
final updated = PathRecord(
|
||||
pathBytes: List<int>.from(pathBytes),
|
||||
hopCount: pathBytes.length ~/ hashSize,
|
||||
pathBytes: normalizedPathBytes,
|
||||
hopCount: normalizedPathBytes.length ~/ hashSize,
|
||||
hashSize: hashSize,
|
||||
source: PathRecordSource.observed,
|
||||
successCount: existing?.successCount ?? 0,
|
||||
failureCount: existing?.failureCount ?? 0,
|
||||
lastRoundTripTimeMs: existing?.lastRoundTripTimeMs ?? 0,
|
||||
@@ -191,6 +199,7 @@ class PathHistoryService {
|
||||
pathBytes: selection.pathBytes.toList(),
|
||||
hopCount: selection.hopCount,
|
||||
hashSize: selection.hashSize,
|
||||
source: existing?.source ?? PathRecordSource.learned,
|
||||
successCount: (existing?.successCount ?? 0) + (success ? 1 : 0),
|
||||
failureCount: (existing?.failureCount ?? 0) + (success ? 0 : 1),
|
||||
lastRoundTripTimeMs: success
|
||||
|
||||
21
lib/services/profile_device_key_resolver.dart
Normal file
21
lib/services/profile_device_key_resolver.dart
Normal file
@@ -0,0 +1,21 @@
|
||||
import '../models/device_info.dart';
|
||||
|
||||
class ProfileDeviceKeyResolver {
|
||||
static String? resolve({
|
||||
required DeviceInfo deviceInfo,
|
||||
required ConnectionMode connectionMode,
|
||||
}) {
|
||||
// Always prefer the public key — it's the only truly stable identifier.
|
||||
// Don't fall back to deviceId to avoid creating duplicate profiles when
|
||||
// publicKey arrives late (after initial connection but before DeviceInfo).
|
||||
final publicKey = deviceInfo.publicKey;
|
||||
if (publicKey == null || publicKey.isEmpty) {
|
||||
return null; // Wait until publicKey is available
|
||||
}
|
||||
|
||||
final hex = publicKey
|
||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||
.join();
|
||||
return hex.isNotEmpty ? 'pk:$hex' : null;
|
||||
}
|
||||
}
|
||||
185
lib/services/profile_manager.dart
Normal file
185
lib/services/profile_manager.dart
Normal file
@@ -0,0 +1,185 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
import '../models/config_profile.dart';
|
||||
import 'profiles_feature_service.dart';
|
||||
|
||||
class ProfileManager with ChangeNotifier {
|
||||
static const String _profilesKey = 'profiles_library';
|
||||
static const String activeProfileIdKey = 'profiles_active_profile_id';
|
||||
static const String _deviceProfileDefaultsKey =
|
||||
'profiles_device_active_profile_ids';
|
||||
static const String _transferHistoryKey = 'profiles_transfer_history';
|
||||
|
||||
final List<ConfigProfile> _customProfiles = <ConfigProfile>[];
|
||||
final List<ProfileTransferRecord> _transferHistory =
|
||||
<ProfileTransferRecord>[];
|
||||
final Map<String, String> _deviceProfileDefaults = <String, String>{};
|
||||
bool _isInitialized = false;
|
||||
bool _profilesEnabled = false;
|
||||
String _activeProfileId = ConfigProfile.defaultProfileId;
|
||||
|
||||
bool get isInitialized => _isInitialized;
|
||||
bool get profilesEnabled => _profilesEnabled;
|
||||
String get activeProfileId => _activeProfileId;
|
||||
List<ConfigProfile> get customProfiles => List.unmodifiable(_customProfiles);
|
||||
List<ProfileTransferRecord> get transferHistory =>
|
||||
List.unmodifiable(_transferHistory);
|
||||
|
||||
List<ConfigProfile> get visibleProfiles => [
|
||||
if (_profilesEnabled) ConfigProfile.defaultProfile(),
|
||||
..._customProfiles,
|
||||
];
|
||||
|
||||
Future<void> initialize() async {
|
||||
if (_isInitialized) return;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
_profilesEnabled = await ProfilesFeatureService.isEnabled();
|
||||
_activeProfileId =
|
||||
prefs.getString(activeProfileIdKey) ?? ConfigProfile.defaultProfileId;
|
||||
|
||||
final deviceDefaultsJson = prefs.getString(_deviceProfileDefaultsKey);
|
||||
if (deviceDefaultsJson != null && deviceDefaultsJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(deviceDefaultsJson);
|
||||
if (decoded is Map<String, dynamic>) {
|
||||
_deviceProfileDefaults
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.map((key, value) => MapEntry(key, value?.toString() ?? ''))
|
||||
..removeWhere((key, value) => value.isEmpty),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
final profilesJson = prefs.getString(_profilesKey);
|
||||
if (profilesJson != null && profilesJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(profilesJson) as List<dynamic>;
|
||||
_customProfiles
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(ConfigProfile.fromJson),
|
||||
);
|
||||
}
|
||||
|
||||
final historyJson = prefs.getString(_transferHistoryKey);
|
||||
if (historyJson != null && historyJson.isNotEmpty) {
|
||||
final decoded = jsonDecode(historyJson) as List<dynamic>;
|
||||
_transferHistory
|
||||
..clear()
|
||||
..addAll(
|
||||
decoded.whereType<Map<String, dynamic>>().map(
|
||||
ProfileTransferRecord.fromJson,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
_isInitialized = true;
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
ConfigProfile? getProfile(String id) {
|
||||
if (id == ConfigProfile.defaultProfileId) {
|
||||
return ConfigProfile.defaultProfile();
|
||||
}
|
||||
for (final profile in _customProfiles) {
|
||||
if (profile.id == id) {
|
||||
return profile;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
Future<void> setProfilesEnabled(bool enabled) async {
|
||||
_profilesEnabled = enabled;
|
||||
await ProfilesFeatureService.setEnabled(enabled);
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileId(String id) async {
|
||||
await setActiveProfileIdForDevice(id);
|
||||
}
|
||||
|
||||
String profileIdForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return _activeProfileId;
|
||||
}
|
||||
return _deviceProfileDefaults[deviceKey] ?? ConfigProfile.defaultProfileId;
|
||||
}
|
||||
|
||||
bool hasProfileForDevice(String? deviceKey) {
|
||||
if (deviceKey == null || deviceKey.isEmpty) {
|
||||
return false;
|
||||
}
|
||||
return _deviceProfileDefaults.containsKey(deviceKey);
|
||||
}
|
||||
|
||||
Future<void> setActiveProfileIdForDevice(
|
||||
String id, {
|
||||
String? deviceKey,
|
||||
}) async {
|
||||
_activeProfileId = id;
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(activeProfileIdKey, id);
|
||||
if (deviceKey != null && deviceKey.isNotEmpty) {
|
||||
_deviceProfileDefaults[deviceKey] = id;
|
||||
await prefs.setString(
|
||||
_deviceProfileDefaultsKey,
|
||||
jsonEncode(_deviceProfileDefaults),
|
||||
);
|
||||
}
|
||||
ProfileStorageScope.setScope(
|
||||
profilesEnabled: _profilesEnabled,
|
||||
activeProfileId: _activeProfileId,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> upsertProfile(ConfigProfile profile) async {
|
||||
final index = _customProfiles.indexWhere((item) => item.id == profile.id);
|
||||
if (index == -1) {
|
||||
_customProfiles.add(profile);
|
||||
} else {
|
||||
_customProfiles[index] = profile;
|
||||
}
|
||||
_customProfiles.sort((a, b) => a.name.compareTo(b.name));
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> deleteProfile(String id) async {
|
||||
_customProfiles.removeWhere((profile) => profile.id == id);
|
||||
await _persistProfiles();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> recordTransfer(ProfileTransferRecord record) async {
|
||||
_transferHistory.insert(0, record);
|
||||
if (_transferHistory.length > 100) {
|
||||
_transferHistory.removeRange(100, _transferHistory.length);
|
||||
}
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_transferHistoryKey,
|
||||
jsonEncode(_transferHistory.map((item) => item.toJson()).toList()),
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _persistProfiles() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(
|
||||
_profilesKey,
|
||||
jsonEncode(_customProfiles.map((profile) => profile.toJson()).toList()),
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user