mirror of
https://github.com/dz0ny/meshcore-sar.git
synced 2026-08-11 16:30:28 +00:00
Compare commits
98 Commits
v2026.0313
...
feat/updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c299349b6a | ||
|
|
ab05f13de9 | ||
|
|
0e61dc5bf1 | ||
|
|
d45924d7b4 | ||
|
|
a24e699e22 | ||
|
|
7486837ad0 | ||
|
|
059125c714 | ||
|
|
9279ff2158 | ||
|
|
dc297b0b9f | ||
|
|
2d24481aba | ||
|
|
20a1df94eb | ||
|
|
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 |
21
.github/workflows/build-artifacts.yml
vendored
21
.github/workflows/build-artifacts.yml
vendored
@@ -40,6 +40,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Cache Android NDK
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -95,6 +98,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Enable Linux desktop
|
||||||
run: flutter config --enable-linux-desktop
|
run: flutter config --enable-linux-desktop
|
||||||
@@ -132,6 +138,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Enable Windows desktop
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
@@ -174,6 +183,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Enable macOS desktop
|
||||||
run: flutter config --enable-macos-desktop
|
run: flutter config --enable-macos-desktop
|
||||||
@@ -181,6 +193,9 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Install macOS native build tools
|
||||||
|
run: brew install automake libtool
|
||||||
|
|
||||||
- name: Build macOS release
|
- name: Build macOS release
|
||||||
run: flutter build macos --release
|
run: flutter build macos --release
|
||||||
|
|
||||||
@@ -216,6 +231,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
@@ -272,6 +290,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: stable
|
channel: stable
|
||||||
cache: true
|
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
|
- name: Enable web
|
||||||
run: flutter config --enable-web
|
run: flutter config --enable-web
|
||||||
|
|||||||
3
.github/workflows/build-multiplatform.yml
vendored
3
.github/workflows/build-multiplatform.yml
vendored
@@ -27,6 +27,9 @@ jobs:
|
|||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
cache: true
|
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
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -28,6 +28,7 @@ migrate_working_dir/
|
|||||||
**/ios/Flutter/.last_build_id
|
**/ios/Flutter/.last_build_id
|
||||||
.dart_tool/
|
.dart_tool/
|
||||||
.flutter-plugins-dependencies
|
.flutter-plugins-dependencies
|
||||||
|
pubspec_overrides.yaml
|
||||||
.pub-cache/
|
.pub-cache/
|
||||||
.pub/
|
.pub/
|
||||||
/build/
|
/build/
|
||||||
|
|||||||
25
CHANGELOG.md
Normal file
25
CHANGELOG.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
## 2026-03-15 to 2026-03-22
|
||||||
|
|
||||||
|
- Device connectivity and routing: added Android USB serial companion support, serial compass support, GPS module controls, per-contact path size selection, and favourites support via firmware flags.
|
||||||
|
- Location and map intelligence: added RSSI-based repeater trilateration and contact location estimates, isolated MET history, matched the official advert import/discovery flow, and refreshed map presentation by removing contact trails.
|
||||||
|
- Telemetry and device info: surfaced self telemetry and sensor defaults, opened device info from signal and battery taps, improved signal chip behavior, fixed duplicate telemetry delivery, and restored telemetry refresh behavior.
|
||||||
|
- Contacts and messaging: added meshcore:// contact sharing and repeater neighbour views, supported MeshCore message links, fixed DM retry behavior, corrected room poster naming, and compacted the recipient selector.
|
||||||
|
- UI polish and platform upkeep: improved device settings layout, tightened sensor and contact presentation, expanded localization coverage, and refreshed platform dependencies and build numbers.
|
||||||
|
|
||||||
|
## Key PRs
|
||||||
|
|
||||||
|
- [#26](https://github.com/dz0ny/meshcore-sar/pull/26) Merge pull request #26 from `dz0ny/feat/sensor-telemetry-preview`
|
||||||
|
|
||||||
|
## 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`
|
||||||
@@ -84,6 +84,8 @@ PODS:
|
|||||||
- Flutter
|
- Flutter
|
||||||
- vibration (3.0.0):
|
- vibration (3.0.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
|
- wakelock_plus (0.0.1):
|
||||||
|
- Flutter
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
||||||
@@ -108,6 +110,7 @@ DEPENDENCIES:
|
|||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
- vibration (from `.symlinks/plugins/vibration/ios`)
|
||||||
|
- wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`)
|
||||||
|
|
||||||
SPEC REPOS:
|
SPEC REPOS:
|
||||||
trunk:
|
trunk:
|
||||||
@@ -161,6 +164,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||||
vibration:
|
vibration:
|
||||||
:path: ".symlinks/plugins/vibration/ios"
|
:path: ".symlinks/plugins/vibration/ios"
|
||||||
|
wakelock_plus:
|
||||||
|
:path: ".symlinks/plugins/wakelock_plus/ios"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
||||||
@@ -189,6 +194,7 @@ SPEC CHECKSUMS:
|
|||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
||||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
||||||
|
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||||
|
|
||||||
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
||||||
|
|
||||||
|
|||||||
@@ -489,7 +489,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -511,7 +511,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -530,7 +530,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -547,7 +547,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -679,7 +679,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -702,7 +702,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 115;
|
CURRENT_PROJECT_VERSION = 124;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>115</string>
|
<string>124</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -5,24 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.001473">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.00024">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.569758">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="1.144858">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="95.271262">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="98.142592">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="91.795184">
|
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="157.12775">
|
||||||
|
|
||||||
<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>' Invalid username and password combination. Used 'hey@dz0n.dev' as the username." />
|
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -1638,5 +1638,252 @@
|
|||||||
"contactFailedToClearRoute": "Pfad konnte nicht gelöscht werden: {error}",
|
"contactFailedToClearRoute": "Pfad konnte nicht gelöscht werden: {error}",
|
||||||
"contactRouteCleared": "Pfad gelöscht",
|
"contactRouteCleared": "Pfad gelöscht",
|
||||||
"contactRouteSet": "Pfad gesetzt: {route}",
|
"contactRouteSet": "Pfad gesetzt: {route}",
|
||||||
"contactFailedToSetRoute": "Pfad konnte nicht gesetzt werden: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2303,5 +2303,252 @@
|
|||||||
"contactFailedToClearRoute": "Αποτυχία εκκαθάρισης διαδρομής: {error}",
|
"contactFailedToClearRoute": "Αποτυχία εκκαθάρισης διαδρομής: {error}",
|
||||||
"contactRouteCleared": "Η διαδρομή εκκαθαρίστηκε",
|
"contactRouteCleared": "Η διαδρομή εκκαθαρίστηκε",
|
||||||
"contactRouteSet": "Ορίστηκε διαδρομή: {route}",
|
"contactRouteSet": "Ορίστηκε διαδρομή: {route}",
|
||||||
"contactFailedToSetRoute": "Αποτυχία ορισμού διαδρομής: {error}"
|
"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,80 +2228,55 @@
|
|||||||
"description": "Message when there are no chat contacts to send to"
|
"description": "Message when there are no chat contacts to send to"
|
||||||
},
|
},
|
||||||
"reply": "Reply",
|
"reply": "Reply",
|
||||||
"@reply": {
|
"@reply": {},
|
||||||
},
|
|
||||||
"technicalDetails": "Technical details",
|
"technicalDetails": "Technical details",
|
||||||
"@technicalDetails": {
|
"@technicalDetails": {},
|
||||||
},
|
|
||||||
"messageTechnicalDetails": "Message technical details",
|
"messageTechnicalDetails": "Message technical details",
|
||||||
"@messageTechnicalDetails": {
|
"@messageTechnicalDetails": {},
|
||||||
},
|
|
||||||
"linkQuality": "Link quality",
|
"linkQuality": "Link quality",
|
||||||
"@linkQuality": {
|
"@linkQuality": {},
|
||||||
},
|
|
||||||
"delivery": "Delivery",
|
"delivery": "Delivery",
|
||||||
"@delivery": {
|
"@delivery": {},
|
||||||
},
|
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"@status": {
|
"@status": {},
|
||||||
},
|
|
||||||
"expectedAckTag": "Expected ACK tag",
|
"expectedAckTag": "Expected ACK tag",
|
||||||
"@expectedAckTag": {
|
"@expectedAckTag": {},
|
||||||
},
|
|
||||||
"roundTrip": "Round-trip",
|
"roundTrip": "Round-trip",
|
||||||
"@roundTrip": {
|
"@roundTrip": {},
|
||||||
},
|
|
||||||
"retryAttempt": "Retry attempt",
|
"retryAttempt": "Retry attempt",
|
||||||
"@retryAttempt": {
|
"@retryAttempt": {},
|
||||||
},
|
|
||||||
"floodFallback": "Flood fallback",
|
"floodFallback": "Flood fallback",
|
||||||
"@floodFallback": {
|
"@floodFallback": {},
|
||||||
},
|
|
||||||
"identity": "Identity",
|
"identity": "Identity",
|
||||||
"@identity": {
|
"@identity": {},
|
||||||
},
|
|
||||||
"messageId": "Message ID",
|
"messageId": "Message ID",
|
||||||
"@messageId": {
|
"@messageId": {},
|
||||||
},
|
|
||||||
"sender": "Sender",
|
"sender": "Sender",
|
||||||
"@sender": {
|
"@sender": {},
|
||||||
},
|
|
||||||
"senderKey": "Sender key",
|
"senderKey": "Sender key",
|
||||||
"@senderKey": {
|
"@senderKey": {},
|
||||||
},
|
|
||||||
"recipient": "Recipient",
|
"recipient": "Recipient",
|
||||||
"@recipient": {
|
"@recipient": {},
|
||||||
},
|
|
||||||
"recipientKey": "Recipient key",
|
"recipientKey": "Recipient key",
|
||||||
"@recipientKey": {
|
"@recipientKey": {},
|
||||||
},
|
|
||||||
"voice": "Voice",
|
"voice": "Voice",
|
||||||
"@voice": {
|
"@voice": {},
|
||||||
},
|
|
||||||
"voiceId": "Voice ID",
|
"voiceId": "Voice ID",
|
||||||
"@voiceId": {
|
"@voiceId": {},
|
||||||
},
|
|
||||||
"envelope": "Envelope",
|
"envelope": "Envelope",
|
||||||
"@envelope": {
|
"@envelope": {},
|
||||||
},
|
|
||||||
"sessionProgress": "Session progress",
|
"sessionProgress": "Session progress",
|
||||||
"@sessionProgress": {
|
"@sessionProgress": {},
|
||||||
},
|
|
||||||
"complete": "Complete",
|
"complete": "Complete",
|
||||||
"@complete": {
|
"@complete": {},
|
||||||
},
|
|
||||||
"rawDump": "Raw dump",
|
"rawDump": "Raw dump",
|
||||||
"@rawDump": {
|
"@rawDump": {},
|
||||||
},
|
|
||||||
"cannotRetryMissingRecipient": "Cannot retry: recipient information missing",
|
"cannotRetryMissingRecipient": "Cannot retry: recipient information missing",
|
||||||
"@cannotRetryMissingRecipient": {
|
"@cannotRetryMissingRecipient": {},
|
||||||
},
|
|
||||||
"voiceUnavailable": "Voice unavailable right now",
|
"voiceUnavailable": "Voice unavailable right now",
|
||||||
"@voiceUnavailable": {
|
"@voiceUnavailable": {},
|
||||||
},
|
|
||||||
"requestingVoice": "Requesting voice",
|
"requestingVoice": "Requesting voice",
|
||||||
"@requestingVoice": {
|
"@requestingVoice": {},
|
||||||
},
|
|
||||||
"device": "device",
|
"device": "device",
|
||||||
"@device": {
|
"@device": {
|
||||||
"description": "Generic device label"
|
"description": "Generic device label"
|
||||||
@@ -2542,5 +2517,258 @@
|
|||||||
"type": "String"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1638,5 +1638,252 @@
|
|||||||
"contactFailedToClearRoute": "No se pudo borrar la ruta: {error}",
|
"contactFailedToClearRoute": "No se pudo borrar la ruta: {error}",
|
||||||
"contactRouteCleared": "Ruta borrada",
|
"contactRouteCleared": "Ruta borrada",
|
||||||
"contactRouteSet": "Ruta establecida: {route}",
|
"contactRouteSet": "Ruta establecida: {route}",
|
||||||
"contactFailedToSetRoute": "No se pudo establecer la ruta: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1641,5 +1641,252 @@
|
|||||||
"contactFailedToClearRoute": "Échec de l'effacement de la route : {error}",
|
"contactFailedToClearRoute": "Échec de l'effacement de la route : {error}",
|
||||||
"contactRouteCleared": "Route effacée",
|
"contactRouteCleared": "Route effacée",
|
||||||
"contactRouteSet": "Route définie : {route}",
|
"contactRouteSet": "Route définie : {route}",
|
||||||
"contactFailedToSetRoute": "Échec du réglage de la route : {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -563,5 +563,252 @@
|
|||||||
"contactFailedToClearRoute": "Brisanje rute nije uspjelo: {error}",
|
"contactFailedToClearRoute": "Brisanje rute nije uspjelo: {error}",
|
||||||
"contactRouteCleared": "Ruta obrisana",
|
"contactRouteCleared": "Ruta obrisana",
|
||||||
"contactRouteSet": "Ruta postavljena: {route}",
|
"contactRouteSet": "Ruta postavljena: {route}",
|
||||||
"contactFailedToSetRoute": "Postavljanje rute nije uspjelo: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1641,5 +1641,252 @@
|
|||||||
"contactFailedToClearRoute": "Impossibile cancellare il percorso: {error}",
|
"contactFailedToClearRoute": "Impossibile cancellare il percorso: {error}",
|
||||||
"contactRouteCleared": "Percorso cancellato",
|
"contactRouteCleared": "Percorso cancellato",
|
||||||
"contactRouteSet": "Percorso impostato: {route}",
|
"contactRouteSet": "Percorso impostato: {route}",
|
||||||
"contactFailedToSetRoute": "Impossibile impostare il percorso: {error}"
|
"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
@@ -1929,4 +1929,773 @@ class AppLocalizationsDe extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Pfad konnte nicht gesetzt werden: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1936,4 +1936,768 @@ class AppLocalizationsEl extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Αποτυχία ορισμού διαδρομής: $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 => 'Δεν υπάρχουν επαφές συσκευής.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1902,4 +1902,764 @@ class AppLocalizationsEn extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Failed to set route: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1932,4 +1932,774 @@ class AppLocalizationsEs extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'No se pudo establecer la ruta: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1936,4 +1936,774 @@ class AppLocalizationsFr extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Échec du réglage de la route : $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1912,4 +1912,767 @@ class AppLocalizationsHr extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Postavljanje rute nije uspjelo: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1927,4 +1927,769 @@ class AppLocalizationsIt extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Impossibile impostare il percorso: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1924,4 +1924,769 @@ class AppLocalizationsPl extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Nie udało się ustawić trasy: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1931,4 +1931,773 @@ class AppLocalizationsPt extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Falha ao definir a rota: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1921,4 +1921,768 @@ class AppLocalizationsRu extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Не удалось задать маршрут: $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 => 'Нет контактов устройства для очистки.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1912,4 +1912,767 @@ class AppLocalizationsSl extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Nastavitev poti ni uspela: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1914,4 +1914,763 @@ class AppLocalizationsTr extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Rota ayarlanamadı: $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.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1927,4 +1927,773 @@ class AppLocalizationsUk extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return 'Не вдалося задати маршрут: $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 =>
|
||||||
|
'Немає контактів пристрою для очищення.';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1827,4 +1827,745 @@ class AppLocalizationsZh extends AppLocalizations {
|
|||||||
String contactFailedToSetRoute(String error) {
|
String contactFailedToSetRoute(String error) {
|
||||||
return '设置路由失败:$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 => '没有设备联系人可清除。';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2328,5 +2328,252 @@
|
|||||||
"contactFailedToClearRoute": "Nie udało się wyczyścić trasy: {error}",
|
"contactFailedToClearRoute": "Nie udało się wyczyścić trasy: {error}",
|
||||||
"contactRouteCleared": "Trasa wyczyszczona",
|
"contactRouteCleared": "Trasa wyczyszczona",
|
||||||
"contactRouteSet": "Ustawiono trasę: {route}",
|
"contactRouteSet": "Ustawiono trasę: {route}",
|
||||||
"contactFailedToSetRoute": "Nie udało się ustawić trasy: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2328,5 +2328,252 @@
|
|||||||
"contactFailedToClearRoute": "Falha ao limpar a rota: {error}",
|
"contactFailedToClearRoute": "Falha ao limpar a rota: {error}",
|
||||||
"contactRouteCleared": "Rota limpa",
|
"contactRouteCleared": "Rota limpa",
|
||||||
"contactRouteSet": "Rota definida: {route}",
|
"contactRouteSet": "Rota definida: {route}",
|
||||||
"contactFailedToSetRoute": "Falha ao definir a rota: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -563,5 +563,252 @@
|
|||||||
"contactFailedToClearRoute": "Не удалось очистить маршрут: {error}",
|
"contactFailedToClearRoute": "Не удалось очистить маршрут: {error}",
|
||||||
"contactRouteCleared": "Маршрут очищен",
|
"contactRouteCleared": "Маршрут очищен",
|
||||||
"contactRouteSet": "Маршрут задан: {route}",
|
"contactRouteSet": "Маршрут задан: {route}",
|
||||||
"contactFailedToSetRoute": "Не удалось задать маршрут: {error}"
|
"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,8 +511,7 @@
|
|||||||
"rawDump": "Surovi izpis",
|
"rawDump": "Surovi izpis",
|
||||||
"cannotRetryMissingRecipient": "Ponovnega pošiljanja ni mogoče: informacije o prejemniku manjkajo",
|
"cannotRetryMissingRecipient": "Ponovnega pošiljanja ni mogoče: informacije o prejemniku manjkajo",
|
||||||
"voiceUnavailable": "Glas trenutno ni na voljo",
|
"voiceUnavailable": "Glas trenutno ni na voljo",
|
||||||
"requestingVoice": "Zahteva za glasom"
|
"requestingVoice": "Zahteva za glasom",
|
||||||
,
|
|
||||||
"device": "naprava",
|
"device": "naprava",
|
||||||
"change": "Spremeni",
|
"change": "Spremeni",
|
||||||
"wizardOverviewDescription": "Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.",
|
"wizardOverviewDescription": "Ta aplikacija združuje sporočanje MeshCore, terenske SAR posodobitve, zemljevide in orodja za napravo na enem mestu.",
|
||||||
@@ -564,5 +563,252 @@
|
|||||||
"contactFailedToClearRoute": "Brisanje poti ni uspelo: {error}",
|
"contactFailedToClearRoute": "Brisanje poti ni uspelo: {error}",
|
||||||
"contactRouteCleared": "Pot izbrisana",
|
"contactRouteCleared": "Pot izbrisana",
|
||||||
"contactRouteSet": "Pot nastavljena: {route}",
|
"contactRouteSet": "Pot nastavljena: {route}",
|
||||||
"contactFailedToSetRoute": "Nastavitev poti ni uspela: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2328,5 +2328,252 @@
|
|||||||
"contactFailedToClearRoute": "Rota temizlenemedi: {error}",
|
"contactFailedToClearRoute": "Rota temizlenemedi: {error}",
|
||||||
"contactRouteCleared": "Rota temizlendi",
|
"contactRouteCleared": "Rota temizlendi",
|
||||||
"contactRouteSet": "Rota ayarlandı: {route}",
|
"contactRouteSet": "Rota ayarlandı: {route}",
|
||||||
"contactFailedToSetRoute": "Rota ayarlanamadı: {error}"
|
"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."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2328,5 +2328,252 @@
|
|||||||
"contactFailedToClearRoute": "Не вдалося очистити маршрут: {error}",
|
"contactFailedToClearRoute": "Не вдалося очистити маршрут: {error}",
|
||||||
"contactRouteCleared": "Маршрут очищено",
|
"contactRouteCleared": "Маршрут очищено",
|
||||||
"contactRouteSet": "Маршрут задано: {route}",
|
"contactRouteSet": "Маршрут задано: {route}",
|
||||||
"contactFailedToSetRoute": "Не вдалося задати маршрут: {error}"
|
"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,80 +2228,55 @@
|
|||||||
"description": "没有可发送的聊天联系人时的消息"
|
"description": "没有可发送的聊天联系人时的消息"
|
||||||
},
|
},
|
||||||
"reply": "回复",
|
"reply": "回复",
|
||||||
"@reply": {
|
"@reply": {},
|
||||||
},
|
|
||||||
"technicalDetails": "技术细节",
|
"technicalDetails": "技术细节",
|
||||||
"@technicalDetails": {
|
"@technicalDetails": {},
|
||||||
},
|
|
||||||
"messageTechnicalDetails": "消息技术细节",
|
"messageTechnicalDetails": "消息技术细节",
|
||||||
"@messageTechnicalDetails": {
|
"@messageTechnicalDetails": {},
|
||||||
},
|
|
||||||
"linkQuality": "链路质量",
|
"linkQuality": "链路质量",
|
||||||
"@linkQuality": {
|
"@linkQuality": {},
|
||||||
},
|
|
||||||
"delivery": "投递",
|
"delivery": "投递",
|
||||||
"@delivery": {
|
"@delivery": {},
|
||||||
},
|
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"@status": {
|
"@status": {},
|
||||||
},
|
|
||||||
"expectedAckTag": "预期的 ACK 标签",
|
"expectedAckTag": "预期的 ACK 标签",
|
||||||
"@expectedAckTag": {
|
"@expectedAckTag": {},
|
||||||
},
|
|
||||||
"roundTrip": "往返时间",
|
"roundTrip": "往返时间",
|
||||||
"@roundTrip": {
|
"@roundTrip": {},
|
||||||
},
|
|
||||||
"retryAttempt": "重试尝试",
|
"retryAttempt": "重试尝试",
|
||||||
"@retryAttempt": {
|
"@retryAttempt": {},
|
||||||
},
|
|
||||||
"floodFallback": "泛洪回退",
|
"floodFallback": "泛洪回退",
|
||||||
"@floodFallback": {
|
"@floodFallback": {},
|
||||||
},
|
|
||||||
"identity": "身份",
|
"identity": "身份",
|
||||||
"@identity": {
|
"@identity": {},
|
||||||
},
|
|
||||||
"messageId": "消息 ID",
|
"messageId": "消息 ID",
|
||||||
"@messageId": {
|
"@messageId": {},
|
||||||
},
|
|
||||||
"sender": "发送者",
|
"sender": "发送者",
|
||||||
"@sender": {
|
"@sender": {},
|
||||||
},
|
|
||||||
"senderKey": "发送者密钥",
|
"senderKey": "发送者密钥",
|
||||||
"@senderKey": {
|
"@senderKey": {},
|
||||||
},
|
|
||||||
"recipient": "接收者",
|
"recipient": "接收者",
|
||||||
"@recipient": {
|
"@recipient": {},
|
||||||
},
|
|
||||||
"recipientKey": "接收者密钥",
|
"recipientKey": "接收者密钥",
|
||||||
"@recipientKey": {
|
"@recipientKey": {},
|
||||||
},
|
|
||||||
"voice": "语音",
|
"voice": "语音",
|
||||||
"@voice": {
|
"@voice": {},
|
||||||
},
|
|
||||||
"voiceId": "语音 ID",
|
"voiceId": "语音 ID",
|
||||||
"@voiceId": {
|
"@voiceId": {},
|
||||||
},
|
|
||||||
"envelope": "信封",
|
"envelope": "信封",
|
||||||
"@envelope": {
|
"@envelope": {},
|
||||||
},
|
|
||||||
"sessionProgress": "会话进度",
|
"sessionProgress": "会话进度",
|
||||||
"@sessionProgress": {
|
"@sessionProgress": {},
|
||||||
},
|
|
||||||
"complete": "完成",
|
"complete": "完成",
|
||||||
"@complete": {
|
"@complete": {},
|
||||||
},
|
|
||||||
"rawDump": "原始数据",
|
"rawDump": "原始数据",
|
||||||
"@rawDump": {
|
"@rawDump": {},
|
||||||
},
|
|
||||||
"cannotRetryMissingRecipient": "无法重试:缺少接收者信息",
|
"cannotRetryMissingRecipient": "无法重试:缺少接收者信息",
|
||||||
"@cannotRetryMissingRecipient": {
|
"@cannotRetryMissingRecipient": {},
|
||||||
},
|
|
||||||
"voiceUnavailable": "当前语音不可用",
|
"voiceUnavailable": "当前语音不可用",
|
||||||
"@voiceUnavailable": {
|
"@voiceUnavailable": {},
|
||||||
},
|
|
||||||
"requestingVoice": "正在请求语音",
|
"requestingVoice": "正在请求语音",
|
||||||
"@requestingVoice": {
|
"@requestingVoice": {},
|
||||||
},
|
|
||||||
"device": "设备",
|
"device": "设备",
|
||||||
"change": "更改",
|
"change": "更改",
|
||||||
"wizardOverviewDescription": "此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。",
|
"wizardOverviewDescription": "此应用将 MeshCore 消息、SAR 现场更新、地图功能和设备工具整合到同一处。",
|
||||||
@@ -2353,5 +2328,252 @@
|
|||||||
"contactFailedToClearRoute": "清除路由失败:{error}",
|
"contactFailedToClearRoute": "清除路由失败:{error}",
|
||||||
"contactRouteCleared": "路由已清除",
|
"contactRouteCleared": "路由已清除",
|
||||||
"contactRouteSet": "路由已设置:{route}",
|
"contactRouteSet": "路由已设置:{route}",
|
||||||
"contactFailedToSetRoute": "设置路由失败:{error}"
|
"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": "没有设备联系人可清除。"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
import 'package:wakelock_plus/wakelock_plus.dart';
|
||||||
import 'providers/connection_provider.dart';
|
import 'providers/connection_provider.dart';
|
||||||
import 'providers/contacts_provider.dart';
|
import 'providers/contacts_provider.dart';
|
||||||
import 'providers/messages_provider.dart';
|
import 'providers/messages_provider.dart';
|
||||||
@@ -23,14 +24,20 @@ import 'services/voice_player_service.dart';
|
|||||||
import 'services/notification_service.dart';
|
import 'services/notification_service.dart';
|
||||||
import 'services/locale_preferences.dart';
|
import 'services/locale_preferences.dart';
|
||||||
import 'services/mesh_map_nodes_service.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/update_checker_service.dart';
|
||||||
import 'services/wizard_preferences.dart';
|
import 'services/wizard_preferences.dart';
|
||||||
|
import 'screens/discovery_screen.dart';
|
||||||
import 'screens/home_screen.dart';
|
import 'screens/home_screen.dart';
|
||||||
import 'screens/welcome_wizard_screen.dart';
|
import 'screens/welcome_wizard_screen.dart';
|
||||||
import 'theme/app_theme.dart';
|
import 'theme/app_theme.dart';
|
||||||
import 'l10n/app_localizations.dart';
|
import 'l10n/app_localizations.dart';
|
||||||
|
|
||||||
void main() {
|
Future<void> main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await WakelockPlus.enable();
|
||||||
runApp(const MeshCoreSarApp());
|
runApp(const MeshCoreSarApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -42,11 +49,13 @@ class MeshCoreSarApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||||
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
AppThemeMode _themeMode = AppThemeMode.system;
|
AppThemeMode _themeMode = AppThemeMode.system;
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _shouldShowPermissionDialog = false;
|
bool _shouldShowPermissionDialog = false;
|
||||||
bool _wizardCompleted = true; // Will be updated in _initializeApp()
|
bool _wizardCompleted = true; // Will be updated in _initializeApp()
|
||||||
|
String? _pendingNotificationPayload;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -55,6 +64,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initializeApp() async {
|
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 _loadThemePreference();
|
||||||
await _loadLocalePreference();
|
await _loadLocalePreference();
|
||||||
|
|
||||||
@@ -66,6 +83,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
|
|
||||||
// Set up notification tap handler for update notifications
|
// Set up notification tap handler for update notifications
|
||||||
NotificationService().onNotificationTapped = _handleNotificationTap;
|
NotificationService().onNotificationTapped = _handleNotificationTap;
|
||||||
|
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
||||||
|
|
||||||
// Check if we need to request location permissions
|
// Check if we need to request location permissions
|
||||||
await _checkLocationPermissions();
|
await _checkLocationPermissions();
|
||||||
@@ -81,6 +99,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
_wizardCompleted = wizardCompleted;
|
_wizardCompleted = wizardCompleted;
|
||||||
_isInitialized = true;
|
_isInitialized = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (_pendingNotificationPayload != null) {
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
final payload = _pendingNotificationPayload;
|
||||||
|
_pendingNotificationPayload = null;
|
||||||
|
_handleNotificationTap(payload);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle notification tap
|
/// Handle notification tap
|
||||||
@@ -89,10 +115,27 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
|
|
||||||
debugPrint('[Main] Notification tapped: $payload');
|
debugPrint('[Main] Notification tapped: $payload');
|
||||||
|
|
||||||
|
if (!_wizardCompleted) {
|
||||||
|
_pendingNotificationPayload = payload;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Handle update notification tap
|
// Handle update notification tap
|
||||||
if (payload.startsWith('update:')) {
|
if (payload.startsWith('update:')) {
|
||||||
final downloadUrl = payload.substring(7); // Remove 'update:' prefix
|
final downloadUrl = payload.substring(7); // Remove 'update:' prefix
|
||||||
_launchUpdateDownload(downloadUrl);
|
_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
|
// SAR and message notifications handled by their respective providers
|
||||||
}
|
}
|
||||||
@@ -242,6 +285,13 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (_) {
|
||||||
|
final manager = ProfileManager();
|
||||||
|
manager.initialize();
|
||||||
|
return manager;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Voice provider (packet reassembly + playback)
|
// Voice provider (packet reassembly + playback)
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
@@ -294,6 +344,39 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
imageProvider: context.read<ip.ImageProvider>(),
|
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>(),
|
||||||
|
voiceProvider: context.read<VoiceProvider>(),
|
||||||
|
imageProvider: context.read<ip.ImageProvider>(),
|
||||||
|
appProvider: appProvider,
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
child: _buildMaterialApp(),
|
child: _buildMaterialApp(),
|
||||||
);
|
);
|
||||||
@@ -304,6 +387,7 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
builder: (context) {
|
builder: (context) {
|
||||||
final systemBrightness = MediaQuery.platformBrightnessOf(context);
|
final systemBrightness = MediaQuery.platformBrightnessOf(context);
|
||||||
final materialApp = MaterialApp(
|
final materialApp = MaterialApp(
|
||||||
|
navigatorKey: _navigatorKey,
|
||||||
key: ValueKey<String?>(
|
key: ValueKey<String?>(
|
||||||
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
|
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -57,9 +57,7 @@ class Channel {
|
|||||||
} else {
|
} else {
|
||||||
// Normal channel: require explicit secret
|
// Normal channel: require explicit secret
|
||||||
if (explicitSecret == null || explicitSecret.length != 16) {
|
if (explicitSecret == null || explicitSecret.length != 16) {
|
||||||
throw ArgumentError(
|
throw ArgumentError('Normal channels require a 16-byte secret');
|
||||||
'Normal channels require a 16-byte secret',
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
return Channel(
|
return Channel(
|
||||||
index: index,
|
index: index,
|
||||||
@@ -78,6 +76,18 @@ class Channel {
|
|||||||
return Uint8List.fromList(digest.bytes.sublist(0, 16));
|
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)
|
/// Create the default public channel (channel 0)
|
||||||
/// Uses the well-known pre-shared key from MeshCore
|
/// Uses the well-known pre-shared key from MeshCore
|
||||||
factory Channel.publicChannel() {
|
factory Channel.publicChannel() {
|
||||||
@@ -85,8 +95,22 @@ class Channel {
|
|||||||
index: 0,
|
index: 0,
|
||||||
name: 'Public Channel',
|
name: 'Public Channel',
|
||||||
secret: Uint8List.fromList([
|
secret: Uint8List.fromList([
|
||||||
0x8b, 0x33, 0x87, 0xe9, 0xc5, 0xcd, 0xea, 0x6a,
|
0x8b,
|
||||||
0xc9, 0xe5, 0xed, 0xba, 0xa1, 0x15, 0xcd, 0x72,
|
0x33,
|
||||||
|
0x87,
|
||||||
|
0xe9,
|
||||||
|
0xc5,
|
||||||
|
0xcd,
|
||||||
|
0xea,
|
||||||
|
0x6a,
|
||||||
|
0xc9,
|
||||||
|
0xe5,
|
||||||
|
0xed,
|
||||||
|
0xba,
|
||||||
|
0xa1,
|
||||||
|
0x15,
|
||||||
|
0xcd,
|
||||||
|
0x72,
|
||||||
]),
|
]),
|
||||||
flags: null,
|
flags: null,
|
||||||
);
|
);
|
||||||
@@ -95,6 +119,9 @@ class Channel {
|
|||||||
/// Check if this is a hash-based channel (name starts with '#')
|
/// Check if this is a hash-based channel (name starts with '#')
|
||||||
bool get isHashChannel => name.startsWith('#');
|
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
|
/// Display name for the channel
|
||||||
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
/// Returns "Public" for channel 0, otherwise returns the custom name or "Channel N"
|
||||||
String get displayName {
|
String get displayName {
|
||||||
@@ -131,12 +158,7 @@ class Channel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy with modified fields
|
/// Create a copy with modified fields
|
||||||
Channel copyWith({
|
Channel copyWith({int? index, String? name, Uint8List? secret, int? flags}) {
|
||||||
int? index,
|
|
||||||
String? name,
|
|
||||||
Uint8List? secret,
|
|
||||||
int? flags,
|
|
||||||
}) {
|
|
||||||
return Channel(
|
return Channel(
|
||||||
index: index ?? this.index,
|
index: index ?? this.index,
|
||||||
name: name ?? this.name,
|
name: name ?? this.name,
|
||||||
|
|||||||
523
lib/models/config_profile.dart
Normal file
523
lib/models/config_profile.dart
Normal file
@@ -0,0 +1,523 @@
|
|||||||
|
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? 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.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 &&
|
||||||
|
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,
|
||||||
|
'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?,
|
||||||
|
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) {
|
static ParsedContactRoute? fromContact(Contact contact) {
|
||||||
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
if (!contact.routeHasPath || contact.routeHopCount == 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
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)
|
/// Direct BLE connection to MeshCore device (default)
|
||||||
ble,
|
ble,
|
||||||
|
|
||||||
/// Act as SSE server - share BLE device with multiple clients
|
|
||||||
sseServer,
|
|
||||||
|
|
||||||
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
|
/// Direct TCP/WiFi connection to MeshCore device (port 5000)
|
||||||
tcp,
|
tcp,
|
||||||
|
|
||||||
|
/// USB serial connection (Android OTG or Web Serial API)
|
||||||
|
usb,
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ConnectionModeExtension on ConnectionMode {
|
extension ConnectionModeExtension on ConnectionMode {
|
||||||
@@ -26,10 +26,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
|||||||
switch (this) {
|
switch (this) {
|
||||||
case ConnectionMode.ble:
|
case ConnectionMode.ble:
|
||||||
return 'Direct (BLE)';
|
return 'Direct (BLE)';
|
||||||
case ConnectionMode.sseServer:
|
|
||||||
return 'Share Device (Server)';
|
|
||||||
case ConnectionMode.tcp:
|
case ConnectionMode.tcp:
|
||||||
return 'Direct (WiFi)';
|
return 'Direct (WiFi)';
|
||||||
|
case ConnectionMode.usb:
|
||||||
|
return 'Direct (Serial)';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +37,10 @@ extension ConnectionModeExtension on ConnectionMode {
|
|||||||
switch (this) {
|
switch (this) {
|
||||||
case ConnectionMode.ble:
|
case ConnectionMode.ble:
|
||||||
return 'Direct BLE connection to MeshCore device';
|
return 'Direct BLE connection to MeshCore device';
|
||||||
case ConnectionMode.sseServer:
|
|
||||||
return 'Share BLE device with multiple clients over network';
|
|
||||||
case ConnectionMode.tcp:
|
case ConnectionMode.tcp:
|
||||||
return 'Direct WiFi/TCP connection to MeshCore device';
|
return 'Direct WiFi/TCP connection to MeshCore device';
|
||||||
|
case ConnectionMode.usb:
|
||||||
|
return 'Direct serial connection to MeshCore device';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -66,6 +66,12 @@ class DeviceInfo {
|
|||||||
final int? advLat;
|
final int? advLat;
|
||||||
final int? advLon;
|
final int? advLon;
|
||||||
final bool? manualAddContacts;
|
final bool? manualAddContacts;
|
||||||
|
final bool? autoAddUsers;
|
||||||
|
final bool? autoAddRepeaters;
|
||||||
|
final bool? autoAddRoomServers;
|
||||||
|
final bool? autoAddSensors;
|
||||||
|
final bool? autoAddOverwriteOldest;
|
||||||
|
final int? autoAddMaxHops;
|
||||||
final int? radioFreq;
|
final int? radioFreq;
|
||||||
final int? radioBw;
|
final int? radioBw;
|
||||||
final int? radioSf;
|
final int? radioSf;
|
||||||
@@ -90,6 +96,7 @@ class DeviceInfo {
|
|||||||
|
|
||||||
// Repeat mode (firmware v9+)
|
// Repeat mode (firmware v9+)
|
||||||
final bool? clientRepeat;
|
final bool? clientRepeat;
|
||||||
|
final int? pathHashMode;
|
||||||
final bool? supportsSpectrumScan;
|
final bool? supportsSpectrumScan;
|
||||||
final int? spectrumScanMinKhz;
|
final int? spectrumScanMinKhz;
|
||||||
final int? spectrumScanMaxKhz;
|
final int? spectrumScanMaxKhz;
|
||||||
@@ -113,6 +120,12 @@ class DeviceInfo {
|
|||||||
this.advLat,
|
this.advLat,
|
||||||
this.advLon,
|
this.advLon,
|
||||||
this.manualAddContacts,
|
this.manualAddContacts,
|
||||||
|
this.autoAddUsers,
|
||||||
|
this.autoAddRepeaters,
|
||||||
|
this.autoAddRoomServers,
|
||||||
|
this.autoAddSensors,
|
||||||
|
this.autoAddOverwriteOldest,
|
||||||
|
this.autoAddMaxHops,
|
||||||
this.radioFreq,
|
this.radioFreq,
|
||||||
this.radioBw,
|
this.radioBw,
|
||||||
this.radioSf,
|
this.radioSf,
|
||||||
@@ -129,6 +142,7 @@ class DeviceInfo {
|
|||||||
this.manufacturerModel,
|
this.manufacturerModel,
|
||||||
this.semanticVersion,
|
this.semanticVersion,
|
||||||
this.clientRepeat,
|
this.clientRepeat,
|
||||||
|
this.pathHashMode,
|
||||||
this.supportsSpectrumScan,
|
this.supportsSpectrumScan,
|
||||||
this.spectrumScanMinKhz,
|
this.spectrumScanMinKhz,
|
||||||
this.spectrumScanMaxKhz,
|
this.spectrumScanMaxKhz,
|
||||||
@@ -239,6 +253,12 @@ class DeviceInfo {
|
|||||||
int? advLat,
|
int? advLat,
|
||||||
int? advLon,
|
int? advLon,
|
||||||
bool? manualAddContacts,
|
bool? manualAddContacts,
|
||||||
|
bool? autoAddUsers,
|
||||||
|
bool? autoAddRepeaters,
|
||||||
|
bool? autoAddRoomServers,
|
||||||
|
bool? autoAddSensors,
|
||||||
|
bool? autoAddOverwriteOldest,
|
||||||
|
int? autoAddMaxHops,
|
||||||
int? radioFreq,
|
int? radioFreq,
|
||||||
int? radioBw,
|
int? radioBw,
|
||||||
int? radioSf,
|
int? radioSf,
|
||||||
@@ -255,6 +275,7 @@ class DeviceInfo {
|
|||||||
String? manufacturerModel,
|
String? manufacturerModel,
|
||||||
String? semanticVersion,
|
String? semanticVersion,
|
||||||
bool? clientRepeat,
|
bool? clientRepeat,
|
||||||
|
int? pathHashMode,
|
||||||
bool? supportsSpectrumScan,
|
bool? supportsSpectrumScan,
|
||||||
int? spectrumScanMinKhz,
|
int? spectrumScanMinKhz,
|
||||||
int? spectrumScanMaxKhz,
|
int? spectrumScanMaxKhz,
|
||||||
@@ -278,6 +299,13 @@ class DeviceInfo {
|
|||||||
advLat: advLat ?? this.advLat,
|
advLat: advLat ?? this.advLat,
|
||||||
advLon: advLon ?? this.advLon,
|
advLon: advLon ?? this.advLon,
|
||||||
manualAddContacts: manualAddContacts ?? this.manualAddContacts,
|
manualAddContacts: manualAddContacts ?? this.manualAddContacts,
|
||||||
|
autoAddUsers: autoAddUsers ?? this.autoAddUsers,
|
||||||
|
autoAddRepeaters: autoAddRepeaters ?? this.autoAddRepeaters,
|
||||||
|
autoAddRoomServers: autoAddRoomServers ?? this.autoAddRoomServers,
|
||||||
|
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
||||||
|
autoAddOverwriteOldest:
|
||||||
|
autoAddOverwriteOldest ?? this.autoAddOverwriteOldest,
|
||||||
|
autoAddMaxHops: autoAddMaxHops ?? this.autoAddMaxHops,
|
||||||
radioFreq: radioFreq ?? this.radioFreq,
|
radioFreq: radioFreq ?? this.radioFreq,
|
||||||
radioBw: radioBw ?? this.radioBw,
|
radioBw: radioBw ?? this.radioBw,
|
||||||
radioSf: radioSf ?? this.radioSf,
|
radioSf: radioSf ?? this.radioSf,
|
||||||
@@ -294,6 +322,7 @@ class DeviceInfo {
|
|||||||
manufacturerModel: manufacturerModel ?? this.manufacturerModel,
|
manufacturerModel: manufacturerModel ?? this.manufacturerModel,
|
||||||
semanticVersion: semanticVersion ?? this.semanticVersion,
|
semanticVersion: semanticVersion ?? this.semanticVersion,
|
||||||
clientRepeat: clientRepeat ?? this.clientRepeat,
|
clientRepeat: clientRepeat ?? this.clientRepeat,
|
||||||
|
pathHashMode: pathHashMode ?? this.pathHashMode,
|
||||||
supportsSpectrumScan: supportsSpectrumScan ?? this.supportsSpectrumScan,
|
supportsSpectrumScan: supportsSpectrumScan ?? this.supportsSpectrumScan,
|
||||||
spectrumScanMinKhz: spectrumScanMinKhz ?? this.spectrumScanMinKhz,
|
spectrumScanMinKhz: spectrumScanMinKhz ?? this.spectrumScanMinKhz,
|
||||||
spectrumScanMaxKhz: spectrumScanMaxKhz ?? this.spectrumScanMaxKhz,
|
spectrumScanMaxKhz: spectrumScanMaxKhz ?? this.spectrumScanMaxKhz,
|
||||||
|
|||||||
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:flutter_map/flutter_map.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import 'map_coordinate_space.dart';
|
||||||
|
import '../utils/custom_map_id.dart';
|
||||||
|
|
||||||
/// Drawing shape type
|
/// Drawing shape type
|
||||||
enum DrawingShapeType {
|
enum DrawingShapeType { line, rectangle }
|
||||||
line,
|
|
||||||
rectangle,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drawing color enum for compact network transmission
|
/// Drawing color enum for compact network transmission
|
||||||
enum DrawingColor {
|
enum DrawingColor { red, blue, green, yellow, orange, purple, pink, cyan }
|
||||||
red, // 0
|
|
||||||
blue, // 1
|
|
||||||
green, // 2
|
|
||||||
yellow, // 3
|
|
||||||
orange, // 4
|
|
||||||
purple, // 5
|
|
||||||
pink, // 6
|
|
||||||
cyan, // 7
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Drawing colors available for user selection
|
/// Drawing colors available for user selection
|
||||||
class DrawingColors {
|
class DrawingColors {
|
||||||
static const List<Color> palette = [
|
static const List<Color> palette = [
|
||||||
Colors.red, // index 0
|
Colors.red,
|
||||||
Colors.blue, // index 1
|
Colors.blue,
|
||||||
Colors.green, // index 2
|
Colors.green,
|
||||||
Colors.yellow, // index 3
|
Colors.yellow,
|
||||||
Colors.orange, // index 4
|
Colors.orange,
|
||||||
Colors.purple, // index 5
|
Colors.purple,
|
||||||
Colors.pink, // index 6
|
Colors.pink,
|
||||||
Colors.cyan, // index 7
|
Colors.cyan,
|
||||||
];
|
];
|
||||||
|
|
||||||
static String colorToName(Color color) {
|
static String colorToName(Color color) {
|
||||||
@@ -45,37 +36,36 @@ class DrawingColors {
|
|||||||
return 'Unknown';
|
return 'Unknown';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert Color to enum index for network transmission
|
|
||||||
static int colorToIndex(Color color) {
|
static int colorToIndex(Color color) {
|
||||||
for (int i = 0; i < palette.length; i++) {
|
for (int i = 0; i < palette.length; i++) {
|
||||||
if (palette[i].toARGB32() == color.toARGB32()) {
|
if (palette[i].toARGB32() == color.toARGB32()) {
|
||||||
return i;
|
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) {
|
static Color indexToColor(int index) {
|
||||||
if (index >= 0 && index < palette.length) {
|
if (index >= 0 && index < palette.length) {
|
||||||
return palette[index];
|
return palette[index];
|
||||||
}
|
}
|
||||||
return palette[0]; // Default to red if invalid index
|
return palette[0];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Base class for map drawings
|
|
||||||
abstract class MapDrawing {
|
abstract class MapDrawing {
|
||||||
final String id;
|
final String id;
|
||||||
final DrawingShapeType type;
|
final DrawingShapeType type;
|
||||||
final Color color;
|
final Color color;
|
||||||
final DateTime createdAt;
|
final DateTime createdAt;
|
||||||
final String? senderName; // Name of sender (null if local drawing)
|
final String? senderName;
|
||||||
final bool isReceived; // True if drawing was received from another node
|
final bool isReceived;
|
||||||
final String? messageId; // ID of the source message (for navigation)
|
final String? messageId;
|
||||||
final bool isShared; // Whether drawing has been broadcast over mesh
|
final bool isShared;
|
||||||
final bool isSent; // Whether this is a sent drawing (vs received)
|
final bool isSent;
|
||||||
final bool isHidden; // Temporary visibility toggle (session only, not persisted)
|
final bool isHidden;
|
||||||
|
final MapCoordinateSpace coordinateSpace;
|
||||||
|
final String? mapId;
|
||||||
|
|
||||||
MapDrawing({
|
MapDrawing({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -88,79 +78,79 @@ abstract class MapDrawing {
|
|||||||
this.isShared = false,
|
this.isShared = false,
|
||||||
this.isSent = false,
|
this.isSent = false,
|
||||||
this.isHidden = 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();
|
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();
|
Map<String, dynamic> toNetworkJson();
|
||||||
|
|
||||||
/// Parse network JSON (compact format)
|
|
||||||
/// senderName and messageId will be populated from packet metadata
|
|
||||||
static MapDrawing? fromNetworkJson(
|
static MapDrawing? fromNetworkJson(
|
||||||
Map<String, dynamic> json, {
|
Map<String, dynamic> json, {
|
||||||
String? senderName,
|
String? senderName,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
|
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||||
|
String? mapId,
|
||||||
}) {
|
}) {
|
||||||
final typeNum = json['t'] as int?;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final type = DrawingShapeType.values[typeNum];
|
final type = DrawingShapeType.values[typeNum];
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DrawingShapeType.line:
|
case DrawingShapeType.line:
|
||||||
return LineDrawing.fromNetworkJson(
|
return LineDrawing.fromNetworkJson(
|
||||||
json,
|
json,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
coordinateSpace: coordinateSpace,
|
||||||
|
mapId: mapId,
|
||||||
);
|
);
|
||||||
case DrawingShapeType.rectangle:
|
case DrawingShapeType.rectangle:
|
||||||
return RectangleDrawing.fromNetworkJson(
|
return RectangleDrawing.fromNetworkJson(
|
||||||
json,
|
json,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
messageId: messageId,
|
messageId: messageId,
|
||||||
|
coordinateSpace: coordinateSpace,
|
||||||
|
mapId: mapId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create from JSON
|
|
||||||
static MapDrawing? fromJson(Map<String, dynamic> json) {
|
static MapDrawing? fromJson(Map<String, dynamic> json) {
|
||||||
final typeStr = json['type'] as String?;
|
final typeStr = json['type'] as String?;
|
||||||
if (typeStr == null) return null;
|
if (typeStr == null) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final type = DrawingShapeType.values.firstWhere(
|
final type = DrawingShapeType.values.firstWhere(
|
||||||
(e) => e.toString() == 'DrawingShapeType.$typeStr',
|
(value) => value.name == typeStr,
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case DrawingShapeType.line:
|
case DrawingShapeType.line:
|
||||||
return LineDrawing.fromJson(json);
|
return LineDrawing.fromJson(json);
|
||||||
case DrawingShapeType.rectangle:
|
case DrawingShapeType.rectangle:
|
||||||
return RectangleDrawing.fromJson(json);
|
return RectangleDrawing.fromJson(json);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the center point of the drawing
|
|
||||||
LatLng getCenter();
|
LatLng getCenter();
|
||||||
|
|
||||||
/// Get the bounds of the drawing
|
|
||||||
LatLngBounds getBounds();
|
LatLngBounds getBounds();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Line drawing on map
|
|
||||||
class LineDrawing extends MapDrawing {
|
class LineDrawing extends MapDrawing {
|
||||||
final List<LatLng> points;
|
final List<LatLng> points;
|
||||||
|
|
||||||
@@ -175,6 +165,8 @@ class LineDrawing extends MapDrawing {
|
|||||||
super.isShared,
|
super.isShared,
|
||||||
super.isSent,
|
super.isSent,
|
||||||
super.isHidden,
|
super.isHidden,
|
||||||
|
super.coordinateSpace,
|
||||||
|
super.mapId,
|
||||||
}) : super(type: DrawingShapeType.line);
|
}) : super(type: DrawingShapeType.line);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -184,31 +176,51 @@ class LineDrawing extends MapDrawing {
|
|||||||
'type': type.name,
|
'type': type.name,
|
||||||
'color': color.toARGB32(),
|
'color': color.toARGB32(),
|
||||||
'createdAt': createdAt.toIso8601String(),
|
'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,
|
'isShared': isShared,
|
||||||
// Note: isHidden is not persisted - it's session-only
|
'coordinateSpace': coordinateSpace.name,
|
||||||
|
'mapId': normalizeCustomMapId(mapId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toNetworkJson() {
|
Map<String, dynamic> toNetworkJson() {
|
||||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), p=points
|
final payload = <String, dynamic>{
|
||||||
// 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 {
|
|
||||||
't': type.index,
|
't': type.index,
|
||||||
'c': DrawingColors.colorToIndex(color),
|
'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) {
|
static LineDrawing fromJson(Map<String, dynamic> json) {
|
||||||
final pointsJson = json['points'] as List<dynamic>;
|
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?;
|
final senderName = json['sender'] as String?;
|
||||||
|
|
||||||
return LineDrawing(
|
return LineDrawing(
|
||||||
@@ -217,8 +229,12 @@ class LineDrawing extends MapDrawing {
|
|||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
points: points,
|
points: points,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
isReceived: senderName != null, // Mark as received if sender is present
|
isReceived: senderName != null,
|
||||||
isShared: json['isShared'] as bool? ?? false,
|
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, {
|
Map<String, dynamic> json, {
|
||||||
String? senderName,
|
String? senderName,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
|
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||||
|
String? mapId,
|
||||||
}) {
|
}) {
|
||||||
// Parse ultra-compact format
|
final flatPoints = (json['p'] as List<dynamic>).cast<num>();
|
||||||
final pointsFlat = (json['p'] as List<dynamic>).cast<double>();
|
|
||||||
final points = <LatLng>[];
|
final points = <LatLng>[];
|
||||||
for (int i = 0; i < pointsFlat.length; i += 2) {
|
for (int i = 0; i < flatPoints.length; i += 2) {
|
||||||
points.add(LatLng(pointsFlat[i], pointsFlat[i + 1]));
|
points.add(
|
||||||
|
LatLng(flatPoints[i].toDouble(), flatPoints[i + 1].toDouble()),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return LineDrawing(
|
return LineDrawing(
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
color: DrawingColors.indexToColor(json['c'] as int),
|
color: DrawingColors.indexToColor(json['c'] as int),
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
points: points,
|
points: points,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
isReceived: true,
|
isReceived: true,
|
||||||
messageId: messageId, // Link to source message
|
messageId: messageId,
|
||||||
isShared: false, // Received drawings are not marked as shared
|
coordinateSpace: coordinateSpace,
|
||||||
|
mapId: normalizeCustomMapId(mapId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy with updated points
|
LineDrawing copyWith({
|
||||||
LineDrawing copyWith({List<LatLng>? points}) {
|
List<LatLng>? points,
|
||||||
|
bool? isHidden,
|
||||||
|
bool? isShared,
|
||||||
|
bool? isReceived,
|
||||||
|
String? messageId,
|
||||||
|
String? senderName,
|
||||||
|
MapCoordinateSpace? coordinateSpace,
|
||||||
|
String? mapId,
|
||||||
|
}) {
|
||||||
return LineDrawing(
|
return LineDrawing(
|
||||||
id: id,
|
id: id,
|
||||||
color: color,
|
color: color,
|
||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
points: points ?? this.points,
|
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
|
@override
|
||||||
LatLng getCenter() {
|
LatLng getCenter() {
|
||||||
if (points.isEmpty) return LatLng(0, 0);
|
if (points.isEmpty) return const LatLng(0, 0);
|
||||||
if (points.length == 1) return points[0];
|
if (points.length == 1) return points[0];
|
||||||
|
|
||||||
// Calculate center as average of all points
|
|
||||||
double sumLat = 0;
|
double sumLat = 0;
|
||||||
double sumLon = 0;
|
double sumLon = 0;
|
||||||
for (final point in points) {
|
for (final point in points) {
|
||||||
@@ -273,8 +307,12 @@ class LineDrawing extends MapDrawing {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
LatLngBounds getBounds() {
|
LatLngBounds getBounds() {
|
||||||
if (points.isEmpty) return LatLngBounds(LatLng(0, 0), LatLng(0, 0));
|
if (points.isEmpty) {
|
||||||
if (points.length == 1) return LatLngBounds(points[0], points[0]);
|
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 minLat = points[0].latitude;
|
||||||
double maxLat = points[0].latitude;
|
double maxLat = points[0].latitude;
|
||||||
@@ -292,7 +330,6 @@ class LineDrawing extends MapDrawing {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rectangle drawing on map
|
|
||||||
class RectangleDrawing extends MapDrawing {
|
class RectangleDrawing extends MapDrawing {
|
||||||
final LatLng topLeft;
|
final LatLng topLeft;
|
||||||
final LatLng bottomRight;
|
final LatLng bottomRight;
|
||||||
@@ -309,16 +346,17 @@ class RectangleDrawing extends MapDrawing {
|
|||||||
super.isShared,
|
super.isShared,
|
||||||
super.isSent,
|
super.isSent,
|
||||||
super.isHidden,
|
super.isHidden,
|
||||||
|
super.coordinateSpace,
|
||||||
|
super.mapId,
|
||||||
}) : super(type: DrawingShapeType.rectangle);
|
}) : super(type: DrawingShapeType.rectangle);
|
||||||
|
|
||||||
/// Get all corner points for rendering
|
|
||||||
List<LatLng> get corners => [
|
List<LatLng> get corners => [
|
||||||
topLeft,
|
topLeft,
|
||||||
LatLng(topLeft.latitude, bottomRight.longitude), // top right
|
LatLng(topLeft.latitude, bottomRight.longitude),
|
||||||
bottomRight,
|
bottomRight,
|
||||||
LatLng(bottomRight.latitude, topLeft.longitude), // bottom left
|
LatLng(bottomRight.latitude, topLeft.longitude),
|
||||||
topLeft, // close the rectangle
|
topLeft,
|
||||||
];
|
];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
@@ -328,27 +366,41 @@ class RectangleDrawing extends MapDrawing {
|
|||||||
'color': color.toARGB32(),
|
'color': color.toARGB32(),
|
||||||
'createdAt': createdAt.toIso8601String(),
|
'createdAt': createdAt.toIso8601String(),
|
||||||
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
'topLeft': {'lat': topLeft.latitude, 'lon': topLeft.longitude},
|
||||||
'bottomRight': {'lat': bottomRight.latitude, 'lon': bottomRight.longitude},
|
'bottomRight': {
|
||||||
|
'lat': bottomRight.latitude,
|
||||||
|
'lon': bottomRight.longitude,
|
||||||
|
},
|
||||||
'isShared': isShared,
|
'isShared': isShared,
|
||||||
// Note: isHidden is not persisted - it's session-only
|
'coordinateSpace': coordinateSpace.name,
|
||||||
|
'mapId': normalizeCustomMapId(mapId),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Map<String, dynamic> toNetworkJson() {
|
Map<String, dynamic> toNetworkJson() {
|
||||||
// Ultra-compact format: t=type (0=line, 1=rect), c=color index (0-7), b=bounds [lat1,lon1,lat2,lon2]
|
final payload = <String, dynamic>{
|
||||||
// Coordinates rounded to 5 decimal places (~1m precision, like SAR markers)
|
|
||||||
// Sender is fetched from packet metadata, not included in JSON
|
|
||||||
return {
|
|
||||||
't': type.index,
|
't': type.index,
|
||||||
'c': DrawingColors.colorToIndex(color),
|
'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) {
|
static RectangleDrawing fromJson(Map<String, dynamic> json) {
|
||||||
@@ -360,11 +412,21 @@ class RectangleDrawing extends MapDrawing {
|
|||||||
id: json['id'] as String,
|
id: json['id'] as String,
|
||||||
color: Color(json['color'] as int),
|
color: Color(json['color'] as int),
|
||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
topLeft: LatLng(topLeftJson['lat'] as double, topLeftJson['lon'] as double),
|
topLeft: LatLng(
|
||||||
bottomRight: LatLng(bottomRightJson['lat'] as double, bottomRightJson['lon'] as double),
|
(topLeftJson['lat'] as num).toDouble(),
|
||||||
|
(topLeftJson['lon'] as num).toDouble(),
|
||||||
|
),
|
||||||
|
bottomRight: LatLng(
|
||||||
|
(bottomRightJson['lat'] as num).toDouble(),
|
||||||
|
(bottomRightJson['lon'] as num).toDouble(),
|
||||||
|
),
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
isReceived: senderName != null, // Mark as received if sender is present
|
isReceived: senderName != null,
|
||||||
isShared: json['isShared'] as bool? ?? false,
|
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, {
|
Map<String, dynamic> json, {
|
||||||
String? senderName,
|
String? senderName,
|
||||||
String? messageId,
|
String? messageId,
|
||||||
|
MapCoordinateSpace coordinateSpace = MapCoordinateSpace.geo,
|
||||||
|
String? mapId,
|
||||||
}) {
|
}) {
|
||||||
// Parse ultra-compact format
|
final bounds = (json['b'] as List<dynamic>).cast<num>();
|
||||||
final bounds = (json['b'] as List<dynamic>).cast<double>();
|
|
||||||
|
|
||||||
return RectangleDrawing(
|
return RectangleDrawing(
|
||||||
id: DateTime.now().millisecondsSinceEpoch.toString(), // Generate new ID
|
id: DateTime.now().millisecondsSinceEpoch.toString(),
|
||||||
color: DrawingColors.indexToColor(json['c'] as int),
|
color: DrawingColors.indexToColor(json['c'] as int),
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
topLeft: LatLng(bounds[0], bounds[1]),
|
topLeft: LatLng(bounds[0].toDouble(), bounds[1].toDouble()),
|
||||||
bottomRight: LatLng(bounds[2], bounds[3]),
|
bottomRight: LatLng(bounds[2].toDouble(), bounds[3].toDouble()),
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
isReceived: true,
|
isReceived: true,
|
||||||
messageId: messageId, // Link to source message
|
messageId: messageId,
|
||||||
isShared: false, // Received drawings are not marked as shared
|
coordinateSpace: coordinateSpace,
|
||||||
|
mapId: normalizeCustomMapId(mapId),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a copy with updated corners
|
|
||||||
RectangleDrawing copyWith({
|
RectangleDrawing copyWith({
|
||||||
LatLng? topLeft,
|
LatLng? topLeft,
|
||||||
LatLng? bottomRight,
|
LatLng? bottomRight,
|
||||||
|
bool? isHidden,
|
||||||
|
bool? isShared,
|
||||||
|
bool? isReceived,
|
||||||
|
String? messageId,
|
||||||
|
String? senderName,
|
||||||
|
MapCoordinateSpace? coordinateSpace,
|
||||||
|
String? mapId,
|
||||||
}) {
|
}) {
|
||||||
return RectangleDrawing(
|
return RectangleDrawing(
|
||||||
id: id,
|
id: id,
|
||||||
@@ -400,12 +470,18 @@ class RectangleDrawing extends MapDrawing {
|
|||||||
createdAt: createdAt,
|
createdAt: createdAt,
|
||||||
topLeft: topLeft ?? this.topLeft,
|
topLeft: topLeft ?? this.topLeft,
|
||||||
bottomRight: bottomRight ?? this.bottomRight,
|
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
|
@override
|
||||||
LatLng getCenter() {
|
LatLng getCenter() {
|
||||||
// Center is the midpoint between top-left and bottom-right
|
|
||||||
return LatLng(
|
return LatLng(
|
||||||
(topLeft.latitude + bottomRight.latitude) / 2,
|
(topLeft.latitude + bottomRight.latitude) / 2,
|
||||||
(topLeft.longitude + bottomRight.longitude) / 2,
|
(topLeft.longitude + bottomRight.longitude) / 2,
|
||||||
@@ -414,7 +490,6 @@ class RectangleDrawing extends MapDrawing {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
LatLngBounds getBounds() {
|
LatLngBounds getBounds() {
|
||||||
// Bounds are simply the two corners
|
|
||||||
return LatLngBounds(topLeft, bottomRight);
|
return LatLngBounds(topLeft, bottomRight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ export 'package:meshcore_client/meshcore_client.dart'
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:meshcore_client/meshcore_client.dart';
|
import 'package:meshcore_client/meshcore_client.dart';
|
||||||
import 'sar_marker.dart';
|
import 'sar_marker.dart';
|
||||||
|
import 'map_coordinate_space.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
|
|
||||||
extension MessageVoiceExtension on Message {
|
extension MessageVoiceExtension on Message {
|
||||||
@@ -47,7 +48,7 @@ extension MessageSarExtension on Message {
|
|||||||
|
|
||||||
/// Convert to a [SarMarker] if this message contains SAR data.
|
/// Convert to a [SarMarker] if this message contains SAR data.
|
||||||
SarMarker? toSarMarker() {
|
SarMarker? toSarMarker() {
|
||||||
if (!isSarMarker || sarMarkerType == null || sarGpsCoordinates == null) {
|
if (!isSarMarker || sarMarkerType == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,16 +58,26 @@ extension MessageSarExtension on Message {
|
|||||||
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
debugPrint(' message.sarMarkerType: $sarMarkerType');
|
||||||
debugPrint(' message.sarCustomEmoji: "$sarCustomEmoji"');
|
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(
|
return SarMarker(
|
||||||
id: id,
|
id: id,
|
||||||
type: sarMarkerType!,
|
type: sarMarkerType!,
|
||||||
location: sarGpsCoordinates!,
|
location: location,
|
||||||
timestamp: sentAt,
|
timestamp: sentAt,
|
||||||
senderPublicKey: senderPublicKeyPrefix,
|
senderPublicKey: senderPublicKeyPrefix,
|
||||||
senderName: senderName,
|
senderName: senderName,
|
||||||
notes: sarNotes,
|
notes: sarNotes,
|
||||||
customEmoji: sarCustomEmoji,
|
customEmoji: sarCustomEmoji,
|
||||||
colorIndex: sarColorIndex,
|
colorIndex: sarColorIndex,
|
||||||
|
coordinateSpace: coordinateSpace,
|
||||||
|
mapId: sarCustomMapId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ class MessageReceptionDetails {
|
|||||||
final int? senderToReceiptMs;
|
final int? senderToReceiptMs;
|
||||||
final int? estimatedTransmitMs;
|
final int? estimatedTransmitMs;
|
||||||
final int? postTransmitDelayMs;
|
final int? postTransmitDelayMs;
|
||||||
|
final int receivedCopies;
|
||||||
|
|
||||||
const MessageReceptionDetails({
|
const MessageReceptionDetails({
|
||||||
required this.capturedAt,
|
required this.capturedAt,
|
||||||
@@ -40,6 +41,7 @@ class MessageReceptionDetails {
|
|||||||
this.senderToReceiptMs,
|
this.senderToReceiptMs,
|
||||||
this.estimatedTransmitMs,
|
this.estimatedTransmitMs,
|
||||||
this.postTransmitDelayMs,
|
this.postTransmitDelayMs,
|
||||||
|
this.receivedCopies = 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
String? get pathBytesHex => pathBytes == null || pathBytes!.isEmpty
|
String? get pathBytesHex => pathBytes == null || pathBytes!.isEmpty
|
||||||
@@ -56,9 +58,60 @@ class MessageReceptionDetails {
|
|||||||
'senderToReceiptMs': senderToReceiptMs,
|
'senderToReceiptMs': senderToReceiptMs,
|
||||||
'estimatedTransmitMs': estimatedTransmitMs,
|
'estimatedTransmitMs': estimatedTransmitMs,
|
||||||
'postTransmitDelayMs': postTransmitDelayMs,
|
'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) {
|
static MessageReceptionDetails? fromJson(Map<String, dynamic> json) {
|
||||||
final capturedAtMillis = json['capturedAtMillis'];
|
final capturedAtMillis = json['capturedAtMillis'];
|
||||||
if (capturedAtMillis is! int) {
|
if (capturedAtMillis is! int) {
|
||||||
@@ -81,6 +134,7 @@ class MessageReceptionDetails {
|
|||||||
senderToReceiptMs: json['senderToReceiptMs'] as int?,
|
senderToReceiptMs: json['senderToReceiptMs'] as int?,
|
||||||
estimatedTransmitMs: json['estimatedTransmitMs'] as int?,
|
estimatedTransmitMs: json['estimatedTransmitMs'] as int?,
|
||||||
postTransmitDelayMs: json['postTransmitDelayMs'] as int?,
|
postTransmitDelayMs: json['postTransmitDelayMs'] as int?,
|
||||||
|
receivedCopies: json['receivedCopies'] as int? ?? 1,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
|
import 'map_coordinate_space.dart';
|
||||||
import '../services/sar_template_service.dart';
|
import '../services/sar_template_service.dart';
|
||||||
|
|
||||||
/// SAR (Search & Rescue) marker types
|
/// SAR (Search & Rescue) marker types
|
||||||
@@ -78,6 +79,8 @@ class SarMarker {
|
|||||||
final String? notes;
|
final String? notes;
|
||||||
final String? customEmoji; // For custom SAR markers not in predefined types
|
final String? customEmoji; // For custom SAR markers not in predefined types
|
||||||
final int? colorIndex; // Color index (0-7) from standard palette
|
final int? colorIndex; // Color index (0-7) from standard palette
|
||||||
|
final MapCoordinateSpace coordinateSpace;
|
||||||
|
final String? mapId;
|
||||||
|
|
||||||
SarMarker({
|
SarMarker({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -89,8 +92,13 @@ class SarMarker {
|
|||||||
this.notes,
|
this.notes,
|
||||||
this.customEmoji,
|
this.customEmoji,
|
||||||
this.colorIndex,
|
this.colorIndex,
|
||||||
|
this.coordinateSpace = MapCoordinateSpace.geo,
|
||||||
|
this.mapId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
bool get isCustomMapMarker =>
|
||||||
|
coordinateSpace == MapCoordinateSpace.customMap && mapId != null;
|
||||||
|
|
||||||
/// Get sender public key as hex string (short)
|
/// Get sender public key as hex string (short)
|
||||||
String? get senderKeyShort {
|
String? get senderKeyShort {
|
||||||
if (senderPublicKey == null || senderPublicKey!.length < 8) return null;
|
if (senderPublicKey == null || senderPublicKey!.length < 8) return null;
|
||||||
@@ -167,6 +175,8 @@ class SarMarker {
|
|||||||
String? notes,
|
String? notes,
|
||||||
String? customEmoji,
|
String? customEmoji,
|
||||||
int? colorIndex,
|
int? colorIndex,
|
||||||
|
MapCoordinateSpace? coordinateSpace,
|
||||||
|
String? mapId,
|
||||||
}) {
|
}) {
|
||||||
return SarMarker(
|
return SarMarker(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
@@ -178,6 +188,8 @@ class SarMarker {
|
|||||||
notes: notes ?? this.notes,
|
notes: notes ?? this.notes,
|
||||||
customEmoji: customEmoji ?? this.customEmoji,
|
customEmoji: customEmoji ?? this.customEmoji,
|
||||||
colorIndex: colorIndex ?? this.colorIndex,
|
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
@@ -7,7 +7,8 @@ class ChannelsProvider with ChangeNotifier {
|
|||||||
int _selectedChannelIndex = 0; // Default to public channel
|
int _selectedChannelIndex = 0; // Default to public channel
|
||||||
|
|
||||||
/// Get all channels
|
/// Get all channels
|
||||||
List<Channel> get channels => _channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
List<Channel> get channels =>
|
||||||
|
_channels.values.toList()..sort((a, b) => a.index.compareTo(b.index));
|
||||||
|
|
||||||
/// Get a specific channel by index
|
/// Get a specific channel by index
|
||||||
Channel? getChannel(int index) => _channels[index];
|
Channel? getChannel(int index) => _channels[index];
|
||||||
@@ -54,12 +55,12 @@ class ChannelsProvider with ChangeNotifier {
|
|||||||
void removeChannel(int index) {
|
void removeChannel(int index) {
|
||||||
if (_channels.containsKey(index)) {
|
if (_channels.containsKey(index)) {
|
||||||
_channels.remove(index);
|
_channels.remove(index);
|
||||||
|
|
||||||
// If the deleted channel was selected, switch to public channel
|
// If the deleted channel was selected, switch to public channel
|
||||||
if (_selectedChannelIndex == index) {
|
if (_selectedChannelIndex == index) {
|
||||||
_selectedChannelIndex = 0;
|
_selectedChannelIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +97,13 @@ class ChannelsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Clear runtime channel state before a live device sync begins.
|
||||||
|
void prepareForDeviceSync() {
|
||||||
|
_channels.clear();
|
||||||
|
_selectedChannelIndex = 0;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if channels have been loaded
|
/// Check if channels have been loaded
|
||||||
bool get hasChannels => _channels.isNotEmpty;
|
bool get hasChannels => _channels.isNotEmpty;
|
||||||
|
|
||||||
|
|||||||
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:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../models/map_drawing.dart';
|
import '../models/map_drawing.dart';
|
||||||
|
import '../models/map_coordinate_space.dart';
|
||||||
|
import '../services/profiles_feature_service.dart';
|
||||||
import '../utils/drawing_message_parser.dart';
|
import '../utils/drawing_message_parser.dart';
|
||||||
|
|
||||||
/// Drawing mode state
|
/// Drawing mode state
|
||||||
@@ -22,6 +25,7 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
|
|
||||||
// Completed drawings
|
// Completed drawings
|
||||||
final List<MapDrawing> _drawings = [];
|
final List<MapDrawing> _drawings = [];
|
||||||
|
bool _isInitialized = false;
|
||||||
|
|
||||||
// In-progress drawing
|
// In-progress drawing
|
||||||
MapDrawing? _currentDrawing;
|
MapDrawing? _currentDrawing;
|
||||||
@@ -32,12 +36,18 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
LatLng? _measurementPoint1;
|
LatLng? _measurementPoint1;
|
||||||
LatLng? _measurementPoint2;
|
LatLng? _measurementPoint2;
|
||||||
double? _measuredDistance; // in meters
|
double? _measuredDistance; // in meters
|
||||||
|
MapCoordinateSpace _activeCoordinateSpace = MapCoordinateSpace.geo;
|
||||||
|
String? _activeMapId;
|
||||||
|
double? _activeMetersPerPixel;
|
||||||
|
|
||||||
// Getters
|
// Getters
|
||||||
DrawingMode get drawingMode => _drawingMode;
|
DrawingMode get drawingMode => _drawingMode;
|
||||||
Color get selectedColor => _selectedColor;
|
Color get selectedColor => _selectedColor;
|
||||||
bool get showReceivedDrawings => _showReceivedDrawings;
|
bool get showReceivedDrawings => _showReceivedDrawings;
|
||||||
bool get showSarMarkers => _showSarMarkers;
|
bool get showSarMarkers => _showSarMarkers;
|
||||||
|
MapCoordinateSpace get activeCoordinateSpace => _activeCoordinateSpace;
|
||||||
|
String? get activeMapId => _activeMapId;
|
||||||
|
double? get activeMetersPerPixel => _activeMetersPerPixel;
|
||||||
List<MapDrawing> get drawings {
|
List<MapDrawing> get drawings {
|
||||||
// Filter out hidden drawings first
|
// Filter out hidden drawings first
|
||||||
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
var visibleDrawings = _drawings.where((d) => !d.isHidden);
|
||||||
@@ -47,12 +57,24 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
visibleDrawings = visibleDrawings.where((d) => !d.isReceived);
|
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());
|
return List.unmodifiable(visibleDrawings.toList());
|
||||||
}
|
}
|
||||||
|
|
||||||
MapDrawing? get currentDrawing => _currentDrawing;
|
MapDrawing? get currentDrawing => _currentDrawing;
|
||||||
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
List<LatLng> get currentLinePoints => List.unmodifiable(_currentLinePoints);
|
||||||
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
LatLng? get rectangleStartPoint => _rectangleStartPoint;
|
||||||
bool get isDrawing => _drawingMode != DrawingMode.none;
|
bool get isDrawing => _drawingMode != DrawingMode.none;
|
||||||
|
bool get isInitialized => _isInitialized;
|
||||||
LatLng? get measurementPoint1 => _measurementPoint1;
|
LatLng? get measurementPoint1 => _measurementPoint1;
|
||||||
LatLng? get measurementPoint2 => _measurementPoint2;
|
LatLng? get measurementPoint2 => _measurementPoint2;
|
||||||
double? get measuredDistance => _measuredDistance;
|
double? get measuredDistance => _measuredDistance;
|
||||||
@@ -61,6 +83,36 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
await _loadPreferences();
|
await _loadPreferences();
|
||||||
await _loadDrawings();
|
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
|
/// Set drawing mode
|
||||||
@@ -95,15 +147,19 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> _loadPreferences() async {
|
Future<void> _loadPreferences() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_showReceivedDrawings = prefs.getBool(_showReceivedDrawingsKey) ?? true;
|
_showReceivedDrawings =
|
||||||
_showSarMarkers = prefs.getBool(_showSarMarkersKey) ?? true;
|
prefs.getBool(_scopedKey(_showReceivedDrawingsKey)) ?? true;
|
||||||
|
_showSarMarkers = prefs.getBool(_scopedKey(_showSarMarkersKey)) ?? true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _savePreferences() async {
|
Future<void> _savePreferences() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_showReceivedDrawingsKey, _showReceivedDrawings);
|
await prefs.setBool(
|
||||||
await prefs.setBool(_showSarMarkersKey, _showSarMarkers);
|
_scopedKey(_showReceivedDrawingsKey),
|
||||||
|
_showReceivedDrawings,
|
||||||
|
);
|
||||||
|
await prefs.setBool(_scopedKey(_showSarMarkersKey), _showSarMarkers);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Start drawing a line
|
/// Start drawing a line
|
||||||
@@ -134,6 +190,10 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
color: _selectedColor,
|
color: _selectedColor,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
points: List.from(_currentLinePoints),
|
points: List.from(_currentLinePoints),
|
||||||
|
coordinateSpace: _activeCoordinateSpace,
|
||||||
|
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||||
|
? _activeMapId
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
_drawings.add(drawing);
|
_drawings.add(drawing);
|
||||||
@@ -177,6 +237,10 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
? _rectangleStartPoint!.longitude
|
? _rectangleStartPoint!.longitude
|
||||||
: endPoint.longitude,
|
: endPoint.longitude,
|
||||||
),
|
),
|
||||||
|
coordinateSpace: _activeCoordinateSpace,
|
||||||
|
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||||
|
? _activeMapId
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -213,6 +277,10 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
topLeft: topLeft,
|
topLeft: topLeft,
|
||||||
bottomRight: bottomRight,
|
bottomRight: bottomRight,
|
||||||
|
coordinateSpace: _activeCoordinateSpace,
|
||||||
|
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||||
|
? _activeMapId
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
_drawings.add(drawing);
|
_drawings.add(drawing);
|
||||||
@@ -234,7 +302,9 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Set second measurement point and calculate distance
|
/// Set second measurement point and calculate distance
|
||||||
void setMeasurementPoint2(LatLng point) {
|
void setMeasurementPoint2(LatLng point) {
|
||||||
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) return;
|
if (_drawingMode != DrawingMode.measure || _measurementPoint1 == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_measurementPoint2 = point;
|
_measurementPoint2 = point;
|
||||||
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
|
_measuredDistance = _calculateDistance(_measurementPoint1!, point);
|
||||||
@@ -243,6 +313,13 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Calculate distance between two points using Haversine formula
|
/// Calculate distance between two points using Haversine formula
|
||||||
double _calculateDistance(LatLng point1, LatLng point2) {
|
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();
|
const Distance distance = Distance();
|
||||||
return distance.as(LengthUnit.Meter, point1, point2);
|
return distance.as(LengthUnit.Meter, point1, point2);
|
||||||
}
|
}
|
||||||
@@ -299,7 +376,7 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonList = _drawings.map((d) => d.toJson()).toList();
|
final jsonList = _drawings.map((d) => d.toJson()).toList();
|
||||||
final jsonString = jsonEncode(jsonList);
|
final jsonString = jsonEncode(jsonList);
|
||||||
await prefs.setString(_storageKey, jsonString);
|
await prefs.setString(_scopedKey(_storageKey), jsonString);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error saving drawings: $e');
|
debugPrint('Error saving drawings: $e');
|
||||||
}
|
}
|
||||||
@@ -309,8 +386,11 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
Future<void> _loadDrawings() async {
|
Future<void> _loadDrawings() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_storageKey);
|
final jsonString = prefs.getString(_scopedKey(_storageKey));
|
||||||
if (jsonString == null) return;
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
|
_drawings.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
final jsonList = jsonDecode(jsonString) as List<dynamic>;
|
||||||
_drawings.clear();
|
_drawings.clear();
|
||||||
@@ -336,6 +416,10 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
color: _selectedColor,
|
color: _selectedColor,
|
||||||
createdAt: DateTime.now(),
|
createdAt: DateTime.now(),
|
||||||
points: _currentLinePoints,
|
points: _currentLinePoints,
|
||||||
|
coordinateSpace: _activeCoordinateSpace,
|
||||||
|
mapId: _activeCoordinateSpace == MapCoordinateSpace.customMap
|
||||||
|
? _activeMapId
|
||||||
|
: null,
|
||||||
);
|
);
|
||||||
} else if (_drawingMode == DrawingMode.rectangle &&
|
} else if (_drawingMode == DrawingMode.rectangle &&
|
||||||
_currentDrawing != null) {
|
_currentDrawing != null) {
|
||||||
@@ -373,6 +457,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: drawing.isShared,
|
isShared: drawing.isShared,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: drawing.isHidden,
|
isHidden: drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
} else if (drawing is RectangleDrawing) {
|
} else if (drawing is RectangleDrawing) {
|
||||||
return RectangleDrawing(
|
return RectangleDrawing(
|
||||||
@@ -387,6 +473,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: drawing.isShared,
|
isShared: drawing.isShared,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: drawing.isHidden,
|
isHidden: drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return drawing;
|
return drawing;
|
||||||
@@ -401,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)
|
/// Get all unshared drawings (local drawings not yet sent)
|
||||||
List<MapDrawing> getUnsharedDrawings() {
|
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
|
/// Mark a drawing as shared
|
||||||
@@ -425,6 +531,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: true,
|
isShared: true,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: drawing.isHidden,
|
isHidden: drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
} else if (drawing is RectangleDrawing) {
|
} else if (drawing is RectangleDrawing) {
|
||||||
_drawings[index] = RectangleDrawing(
|
_drawings[index] = RectangleDrawing(
|
||||||
@@ -439,6 +547,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: true,
|
isShared: true,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: drawing.isHidden,
|
isHidden: drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -466,6 +576,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: drawing.isShared,
|
isShared: drawing.isShared,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: !drawing.isHidden,
|
isHidden: !drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
} else if (drawing is RectangleDrawing) {
|
} else if (drawing is RectangleDrawing) {
|
||||||
_drawings[index] = RectangleDrawing(
|
_drawings[index] = RectangleDrawing(
|
||||||
@@ -480,6 +592,8 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
isShared: drawing.isShared,
|
isShared: drawing.isShared,
|
||||||
isSent: drawing.isSent,
|
isSent: drawing.isSent,
|
||||||
isHidden: !drawing.isHidden,
|
isHidden: !drawing.isHidden,
|
||||||
|
coordinateSpace: drawing.coordinateSpace,
|
||||||
|
mapId: drawing.mapId,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'dart:typed_data';
|
import 'dart:typed_data';
|
||||||
|
|
||||||
/// Message delivery tracking helper
|
/// Message delivery tracking helper
|
||||||
@@ -30,6 +31,9 @@ class MessageDeliveryTracker {
|
|||||||
/// Map of ACK tag to timestamp for timeout cleanup
|
/// Map of ACK tag to timestamp for timeout cleanup
|
||||||
final Map<int, DateTime> _ackTagTimestamps = {};
|
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
|
/// Track a pending message ID before sending
|
||||||
///
|
///
|
||||||
/// This is called BEFORE sending the message. When RESP_CODE_SENT
|
/// This is called BEFORE sending the message. When RESP_CODE_SENT
|
||||||
@@ -112,6 +116,7 @@ class MessageDeliveryTracker {
|
|||||||
_messageIdToAckTag.remove(messageId);
|
_messageIdToAckTag.remove(messageId);
|
||||||
}
|
}
|
||||||
_ackTagTimestamps.remove(ackCode);
|
_ackTagTimestamps.remove(ackCode);
|
||||||
|
_notifySlotFreed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Remove ACK tag mapping by message ID
|
/// Remove ACK tag mapping by message ID
|
||||||
@@ -122,6 +127,7 @@ class MessageDeliveryTracker {
|
|||||||
if (ackTag != null) {
|
if (ackTag != null) {
|
||||||
_ackTagToMessageId.remove(ackTag);
|
_ackTagToMessageId.remove(ackTag);
|
||||||
_ackTagTimestamps.remove(ackTag);
|
_ackTagTimestamps.remove(ackTag);
|
||||||
|
_notifySlotFreed();
|
||||||
}
|
}
|
||||||
_pendingMessageIds.remove(messageId);
|
_pendingMessageIds.remove(messageId);
|
||||||
final emptyKeys = <String>[];
|
final emptyKeys = <String>[];
|
||||||
@@ -166,6 +172,7 @@ class MessageDeliveryTracker {
|
|||||||
_ackTagToMessageId.clear();
|
_ackTagToMessageId.clear();
|
||||||
_messageIdToAckTag.clear();
|
_messageIdToAckTag.clear();
|
||||||
_ackTagTimestamps.clear();
|
_ackTagTimestamps.clear();
|
||||||
|
_notifySlotFreed();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get count of pending ACK tags
|
/// Get count of pending ACK tags
|
||||||
@@ -179,6 +186,27 @@ class MessageDeliveryTracker {
|
|||||||
/// Returns true if >= 7 pending ACKs (stay under firmware limit of 8)
|
/// Returns true if >= 7 pending ACKs (stay under firmware limit of 8)
|
||||||
bool get shouldRateLimit => pendingCount >= 7;
|
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)
|
/// Get oldest pending ACK timestamp (for debugging)
|
||||||
DateTime? get oldestPendingTimestamp {
|
DateTime? get oldestPendingTimestamp {
|
||||||
if (_ackTagTimestamps.isEmpty) return null;
|
if (_ackTagTimestamps.isEmpty) return null;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import '../../models/message.dart';
|
import '../../models/message.dart';
|
||||||
import '../../models/contact.dart';
|
import '../../models/contact.dart';
|
||||||
@@ -19,10 +20,19 @@ class MessageRetryManager {
|
|||||||
final Map<String, DateTime> _lastRetryTimes = {};
|
final Map<String, DateTime> _lastRetryTimes = {};
|
||||||
final Map<String, int> _pathFailureStreaks = {};
|
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.
|
// 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 _defaultLoRaSf = 10;
|
||||||
static const int _defaultLoRaCr = 5;
|
static const int _defaultLoRaCr = 5;
|
||||||
static const int _defaultLoRaBwHz = 250000;
|
static const int _defaultLoRaBwHz = 250000;
|
||||||
@@ -38,32 +48,57 @@ class MessageRetryManager {
|
|||||||
return _retryDelays[attempt];
|
return _retryDelays[attempt];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Calculate a conservative delivery-ACK timeout when firmware doesn't
|
static final math.Random _rng = math.Random();
|
||||||
/// provide one or returns an invalid value.
|
|
||||||
|
/// 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({
|
int calculateAckTimeoutMs({
|
||||||
required String text,
|
required String text,
|
||||||
required Contact? contact,
|
required Contact? contact,
|
||||||
int? suggestedTimeoutMs,
|
int? suggestedTimeoutMs,
|
||||||
}) {
|
}) {
|
||||||
|
int baseTimeout;
|
||||||
if (suggestedTimeoutMs != null && suggestedTimeoutMs > 0) {
|
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;
|
// Add random jitter: 500-2000ms to avoid collision
|
||||||
final airtimeMs = _estimateLoRaAirtimeMs(payloadBytes);
|
final jitterMs = 500 + _rng.nextInt(1501);
|
||||||
final hopCount = contact?.routeHasPath == true
|
return baseTimeout + jitterMs;
|
||||||
? contact!.routeHopCount
|
}
|
||||||
: -1;
|
|
||||||
|
|
||||||
if (hopCount < 0) {
|
/// Max attempts for a given contact based on whether it has a known path.
|
||||||
return ((airtimeMs * 10) + 4000).clamp(10000, 30000);
|
static int maxRetryAttemptsForContact(Contact? contact) {
|
||||||
}
|
final hasPath = contact?.routeHasPath ?? false;
|
||||||
|
return hasPath ? maxRetryAttemptsWithPath : maxRetryAttemptsFloodOnly;
|
||||||
return ((airtimeMs * (hopCount + 1) * 2) + 1500).clamp(4000, 20000);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canRetry(Message message, Contact contact) {
|
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
|
/// Track a retry attempt for a message
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'dart:convert';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../services/profiles_feature_service.dart';
|
||||||
import 'helpers/raw_session_retransmit.dart';
|
import 'helpers/raw_session_retransmit.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
|
|
||||||
@@ -75,6 +76,8 @@ class ImageProvider with ChangeNotifier {
|
|||||||
_restore();
|
_restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _scopedStorageKey() => ProfileStorageScope.scopedKey(_storageKey);
|
||||||
|
|
||||||
// ── Accessors ────────────────────────────────────────────────────────────
|
// ── Accessors ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
ImageSession? session(String sessionId) => _sessions[sessionId];
|
ImageSession? session(String sessionId) => _sessions[sessionId];
|
||||||
@@ -292,18 +295,22 @@ class ImageProvider with ChangeNotifier {
|
|||||||
// ── Persistence ──────────────────────────────────────────────────────────
|
// ── Persistence ──────────────────────────────────────────────────────────
|
||||||
|
|
||||||
Future<void> clearAll() async {
|
Future<void> clearAll() async {
|
||||||
_sessions.clear();
|
_resetInMemoryState();
|
||||||
_outgoing.clear();
|
|
||||||
_ignoredIncomingSessions.clear();
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_storageKey);
|
await prefs.remove(_scopedStorageKey());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ImageProvider] Failed to clear storage: $e');
|
debugPrint('❌ [ImageProvider] Failed to clear storage: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reloadProfileScopedState() async {
|
||||||
|
_resetInMemoryState();
|
||||||
|
await _restore();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void _evictExpiredOutgoing() {
|
void _evictExpiredOutgoing() {
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
_outgoing.removeWhere((_, s) => now.difference(s.cachedAt) > _outgoingTtl);
|
_outgoing.removeWhere((_, s) => now.difference(s.cachedAt) > _outgoingTtl);
|
||||||
@@ -343,7 +350,7 @@ class ImageProvider with ChangeNotifier {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
};
|
};
|
||||||
await prefs.setString(_storageKey, jsonEncode(payload));
|
await prefs.setString(_scopedStorageKey(), jsonEncode(payload));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ImageProvider] Failed to persist: $e');
|
debugPrint('❌ [ImageProvider] Failed to persist: $e');
|
||||||
}
|
}
|
||||||
@@ -352,7 +359,7 @@ class ImageProvider with ChangeNotifier {
|
|||||||
Future<void> _restore() async {
|
Future<void> _restore() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final raw = prefs.getString(_storageKey);
|
final raw = prefs.getString(_scopedStorageKey());
|
||||||
if (raw == null || raw.isEmpty) return;
|
if (raw == null || raw.isEmpty) return;
|
||||||
|
|
||||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
@@ -428,6 +435,12 @@ class ImageProvider with ChangeNotifier {
|
|||||||
debugPrint('❌ [ImageProvider] Failed to restore: $e');
|
debugPrint('❌ [ImageProvider] Failed to restore: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetInMemoryState() {
|
||||||
|
_sessions.clear();
|
||||||
|
_outgoing.clear();
|
||||||
|
_ignoredIncomingSessions.clear();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _OutgoingSession {
|
class _OutgoingSession {
|
||||||
|
|||||||
@@ -1,30 +1,46 @@
|
|||||||
import 'dart:async';
|
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/foundation.dart';
|
||||||
import 'package:flutter_map/flutter_map.dart';
|
import 'package:flutter_map/flutter_map.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
import '../models/custom_map_config.dart';
|
||||||
import '../models/location_trail.dart';
|
import '../models/location_trail.dart';
|
||||||
|
import '../models/map_coordinate_space.dart';
|
||||||
import '../models/map_drawing.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 {
|
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() {
|
MapProvider() {
|
||||||
unawaited(_loadInitialState());
|
unawaited(_loadInitialState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final ImagePicker _imagePicker = ImagePicker();
|
||||||
|
|
||||||
LatLng? _targetLocation;
|
LatLng? _targetLocation;
|
||||||
|
LatLngBounds? _targetBounds;
|
||||||
double? _targetZoom;
|
double? _targetZoom;
|
||||||
bool _shouldAnimate = false;
|
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;
|
LocationTrail? _currentTrail;
|
||||||
bool _isTrailVisible = true;
|
bool _isTrailVisible = true;
|
||||||
final List<LocationTrail> _trailHistory = [];
|
final List<LocationTrail> _trailHistory = [];
|
||||||
|
|
||||||
// WMS overlay toggles
|
|
||||||
bool _showCadastralOverlay = false;
|
bool _showCadastralOverlay = false;
|
||||||
bool _showForestRoadsOverlay = false;
|
bool _showForestRoadsOverlay = false;
|
||||||
bool _showHikingTrailsOverlay = false;
|
bool _showHikingTrailsOverlay = false;
|
||||||
@@ -37,29 +53,27 @@ class MapProvider with ChangeNotifier {
|
|||||||
bool _showPlaceNamesOverlay = false;
|
bool _showPlaceNamesOverlay = false;
|
||||||
bool _showMunicipalityBordersOverlay = false;
|
bool _showMunicipalityBordersOverlay = false;
|
||||||
|
|
||||||
// Contact trail toggles
|
|
||||||
bool _showAllContactTrails = true; // Default to showing all contact trails
|
|
||||||
bool _hideRepeatersOnMap = false;
|
bool _hideRepeatersOnMap = false;
|
||||||
|
|
||||||
// Imported trail (from GPX)
|
|
||||||
LocationTrail? _importedTrail;
|
LocationTrail? _importedTrail;
|
||||||
|
|
||||||
// Download area selection
|
|
||||||
bool _isSelectingDownloadArea = false;
|
bool _isSelectingDownloadArea = false;
|
||||||
LatLngBounds? _downloadAreaBounds;
|
LatLngBounds? _downloadAreaBounds;
|
||||||
|
|
||||||
|
CustomMapConfig? _customMapConfig;
|
||||||
|
bool _isUsingCustomMap = false;
|
||||||
|
|
||||||
LatLng? get targetLocation => _targetLocation;
|
LatLng? get targetLocation => _targetLocation;
|
||||||
|
LatLngBounds? get targetBounds => _targetBounds;
|
||||||
double? get targetZoom => _targetZoom;
|
double? get targetZoom => _targetZoom;
|
||||||
bool get shouldAnimate => _shouldAnimate;
|
bool get shouldAnimate => _shouldAnimate;
|
||||||
Set<String> get visibleContactPaths => Set.unmodifiable(_visibleContactPaths);
|
MapCoordinateSpace get targetCoordinateSpace => _targetCoordinateSpace;
|
||||||
|
String? get targetMapId => _targetMapId;
|
||||||
// Trail getters
|
|
||||||
LocationTrail? get currentTrail => _currentTrail;
|
LocationTrail? get currentTrail => _currentTrail;
|
||||||
bool get isTrailVisible => _isTrailVisible;
|
bool get isTrailVisible => _isTrailVisible;
|
||||||
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
List<LocationTrail> get trailHistory => List.unmodifiable(_trailHistory);
|
||||||
bool get isTrailActive => _currentTrail?.isActive ?? false;
|
bool get isTrailActive => _currentTrail?.isActive ?? false;
|
||||||
|
|
||||||
// WMS overlay getters
|
|
||||||
bool get showCadastralOverlay => _showCadastralOverlay;
|
bool get showCadastralOverlay => _showCadastralOverlay;
|
||||||
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
bool get showForestRoadsOverlay => _showForestRoadsOverlay;
|
||||||
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
|
bool get showHikingTrailsOverlay => _showHikingTrailsOverlay;
|
||||||
@@ -72,69 +86,138 @@ class MapProvider with ChangeNotifier {
|
|||||||
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
bool get showPlaceNamesOverlay => _showPlaceNamesOverlay;
|
||||||
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
bool get showMunicipalityBordersOverlay => _showMunicipalityBordersOverlay;
|
||||||
|
|
||||||
// Contact trail getters
|
|
||||||
bool get showAllContactTrails => _showAllContactTrails;
|
|
||||||
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
bool get hideRepeatersOnMap => _hideRepeatersOnMap;
|
||||||
|
|
||||||
// Imported trail getters
|
|
||||||
LocationTrail? get importedTrail => _importedTrail;
|
LocationTrail? get importedTrail => _importedTrail;
|
||||||
|
|
||||||
// Download area getters
|
|
||||||
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
|
bool get isSelectingDownloadArea => _isSelectingDownloadArea;
|
||||||
LatLngBounds? get downloadAreaBounds => _downloadAreaBounds;
|
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({
|
void navigateToLocation({
|
||||||
required LatLng location,
|
required LatLng location,
|
||||||
double zoom = 15.0,
|
double zoom = 15.0,
|
||||||
bool animate = true,
|
bool animate = true,
|
||||||
}) {
|
}) {
|
||||||
|
if (_isUsingCustomMap) {
|
||||||
|
_isUsingCustomMap = false;
|
||||||
|
unawaited(_saveCustomMapState());
|
||||||
|
}
|
||||||
_targetLocation = location;
|
_targetLocation = location;
|
||||||
|
_targetBounds = null;
|
||||||
_targetZoom = zoom;
|
_targetZoom = zoom;
|
||||||
_shouldAnimate = animate;
|
_shouldAnimate = animate;
|
||||||
|
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||||
|
_targetMapId = null;
|
||||||
notifyListeners();
|
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() {
|
void clearNavigation() {
|
||||||
_targetLocation = null;
|
_targetLocation = null;
|
||||||
|
_targetBounds = null;
|
||||||
_targetZoom = null;
|
_targetZoom = null;
|
||||||
_shouldAnimate = false;
|
_shouldAnimate = false;
|
||||||
// Don't notify listeners to avoid rebuilds
|
_targetCoordinateSpace = MapCoordinateSpace.geo;
|
||||||
|
_targetMapId = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Navigate to a drawing by its ID
|
String? navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
||||||
void navigateToDrawing(String drawingId, dynamic drawingProvider) {
|
final drawing = drawingProvider.getDrawingById(drawingId) as MapDrawing?;
|
||||||
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,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (drawing == null) {
|
if (drawing == null) {
|
||||||
debugPrint('⚠️ [MapProvider] Drawing $drawingId not found');
|
return 'Drawing not found.';
|
||||||
debugPrint('⚠️ [MapProvider] Available drawing IDs: ${drawings.map((d) => d.id).toList()}');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use MapDrawing's built-in getCenter and getBounds methods
|
if (drawing.coordinateSpace == MapCoordinateSpace.customMap) {
|
||||||
final center = drawing.getCenter();
|
if (!matchesActiveCustomMap(drawing.mapId)) {
|
||||||
final bounds = drawing.getBounds();
|
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
|
final bounds = drawing.getBounds();
|
||||||
// For larger drawings, use lower zoom to fit the whole drawing
|
|
||||||
// For smaller drawings, use higher zoom for better detail
|
|
||||||
final latDiff = (bounds.north - bounds.south).abs();
|
final latDiff = (bounds.north - bounds.south).abs();
|
||||||
final lonDiff = (bounds.east - bounds.west).abs();
|
final lonDiff = (bounds.east - bounds.west).abs();
|
||||||
final maxDiff = latDiff > lonDiff ? latDiff : lonDiff;
|
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;
|
double zoom = 15.0;
|
||||||
if (maxDiff < 0.001) {
|
if (maxDiff < 0.001) {
|
||||||
zoom = 17.0;
|
zoom = 17.0;
|
||||||
@@ -150,9 +233,125 @@ class MapProvider with ChangeNotifier {
|
|||||||
zoom = 10.0;
|
zoom = 10.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
final typeStr = drawing is LineDrawing ? 'line' : 'rectangle';
|
navigateToLocation(
|
||||||
debugPrint('🗺️ [MapProvider] Navigating to drawing: $typeStr, zoom: $zoom');
|
location: drawing.getCenter(),
|
||||||
navigateToLocation(location: center, zoom: zoom, animate: true);
|
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) {
|
void updateZoom(double zoom) {
|
||||||
@@ -160,37 +359,7 @@ class MapProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle path visibility for a contact
|
|
||||||
void toggleContactPath(String publicKeyHex) {
|
|
||||||
if (_visibleContactPaths.contains(publicKeyHex)) {
|
|
||||||
_visibleContactPaths.remove(publicKeyHex);
|
|
||||||
} else {
|
|
||||||
_visibleContactPaths.add(publicKeyHex);
|
|
||||||
}
|
|
||||||
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() {
|
void startTrail() {
|
||||||
// End current trail if active
|
|
||||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||||
endTrail();
|
endTrail();
|
||||||
}
|
}
|
||||||
@@ -203,22 +372,22 @@ class MapProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a point to the current trail
|
|
||||||
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
|
void addTrailPoint(LatLng position, {double? accuracy, double? speed}) {
|
||||||
if (_currentTrail == null || !_currentTrail!.isActive) {
|
if (_currentTrail == null || !_currentTrail!.isActive) {
|
||||||
startTrail();
|
startTrail();
|
||||||
}
|
}
|
||||||
|
|
||||||
_currentTrail!.addPoint(TrailPoint(
|
_currentTrail!.addPoint(
|
||||||
position: position,
|
TrailPoint(
|
||||||
timestamp: DateTime.now(),
|
position: position,
|
||||||
accuracy: accuracy,
|
timestamp: DateTime.now(),
|
||||||
speed: speed,
|
accuracy: accuracy,
|
||||||
));
|
speed: speed,
|
||||||
|
),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// End the current trail
|
|
||||||
void endTrail() {
|
void endTrail() {
|
||||||
if (_currentTrail != null) {
|
if (_currentTrail != null) {
|
||||||
_currentTrail!.isActive = false;
|
_currentTrail!.isActive = false;
|
||||||
@@ -231,13 +400,11 @@ class MapProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle trail visibility
|
|
||||||
void toggleTrailVisibility() {
|
void toggleTrailVisibility() {
|
||||||
_isTrailVisible = !_isTrailVisible;
|
_isTrailVisible = !_isTrailVisible;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the current trail
|
|
||||||
void clearCurrentTrail() {
|
void clearCurrentTrail() {
|
||||||
if (_currentTrail != null) {
|
if (_currentTrail != null) {
|
||||||
_currentTrail = null;
|
_currentTrail = null;
|
||||||
@@ -245,158 +412,177 @@ class MapProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all trail history
|
|
||||||
void clearAllTrails() {
|
void clearAllTrails() {
|
||||||
_currentTrail = null;
|
_currentTrail = null;
|
||||||
_trailHistory.clear();
|
_trailHistory.clear();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get total trail distance in meters
|
|
||||||
double get totalTrailDistance {
|
double get totalTrailDistance {
|
||||||
if (_currentTrail == null) return 0;
|
if (_currentTrail == null) return 0;
|
||||||
return _currentTrail!.totalDistance;
|
return _currentTrail!.totalDistance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get trail duration
|
|
||||||
Duration get trailDuration {
|
Duration get trailDuration {
|
||||||
if (_currentTrail == null) return Duration.zero;
|
if (_currentTrail == null) return Duration.zero;
|
||||||
return _currentTrail!.duration;
|
return _currentTrail!.duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle cadastral parcels overlay
|
|
||||||
Future<void> toggleCadastralOverlay() async {
|
Future<void> toggleCadastralOverlay() async {
|
||||||
_showCadastralOverlay = !_showCadastralOverlay;
|
_showCadastralOverlay = !_showCadastralOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle forest roads overlay
|
|
||||||
Future<void> toggleForestRoadsOverlay() async {
|
Future<void> toggleForestRoadsOverlay() async {
|
||||||
_showForestRoadsOverlay = !_showForestRoadsOverlay;
|
_showForestRoadsOverlay = !_showForestRoadsOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle hiking trails overlay
|
|
||||||
Future<void> toggleHikingTrailsOverlay() async {
|
Future<void> toggleHikingTrailsOverlay() async {
|
||||||
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
|
_showHikingTrailsOverlay = !_showHikingTrailsOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle main roads overlay
|
|
||||||
Future<void> toggleMainRoadsOverlay() async {
|
Future<void> toggleMainRoadsOverlay() async {
|
||||||
_showMainRoadsOverlay = !_showMainRoadsOverlay;
|
_showMainRoadsOverlay = !_showMainRoadsOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle house numbers overlay
|
|
||||||
Future<void> toggleHouseNumbersOverlay() async {
|
Future<void> toggleHouseNumbersOverlay() async {
|
||||||
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
|
_showHouseNumbersOverlay = !_showHouseNumbersOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle fire hazard zones overlay
|
|
||||||
Future<void> toggleFireHazardZonesOverlay() async {
|
Future<void> toggleFireHazardZonesOverlay() async {
|
||||||
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
|
_showFireHazardZonesOverlay = !_showFireHazardZonesOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle historical fires overlay
|
|
||||||
Future<void> toggleHistoricalFiresOverlay() async {
|
Future<void> toggleHistoricalFiresOverlay() async {
|
||||||
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
|
_showHistoricalFiresOverlay = !_showHistoricalFiresOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle firebreaks overlay
|
|
||||||
Future<void> toggleFirebreaksOverlay() async {
|
Future<void> toggleFirebreaksOverlay() async {
|
||||||
_showFirebreaksOverlay = !_showFirebreaksOverlay;
|
_showFirebreaksOverlay = !_showFirebreaksOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle Kras fire zones overlay
|
|
||||||
Future<void> toggleKrasFireZonesOverlay() async {
|
Future<void> toggleKrasFireZonesOverlay() async {
|
||||||
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
|
_showKrasFireZonesOverlay = !_showKrasFireZonesOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle place names overlay
|
|
||||||
Future<void> togglePlaceNamesOverlay() async {
|
Future<void> togglePlaceNamesOverlay() async {
|
||||||
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
|
_showPlaceNamesOverlay = !_showPlaceNamesOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle municipality borders overlay
|
|
||||||
Future<void> toggleMunicipalityBordersOverlay() async {
|
Future<void> toggleMunicipalityBordersOverlay() async {
|
||||||
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
|
_showMunicipalityBordersOverlay = !_showMunicipalityBordersOverlay;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
await _saveOverlayState();
|
await _saveOverlayState();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Load overlay state from SharedPreferences
|
|
||||||
Future<void> loadOverlayState() async {
|
Future<void> loadOverlayState() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_showCadastralOverlay = prefs.getBool('map_show_cadastral_overlay') ?? false;
|
_showCadastralOverlay =
|
||||||
_showForestRoadsOverlay = prefs.getBool('map_show_forest_roads_overlay') ?? false;
|
prefs.getBool(_scopedKey('map_show_cadastral_overlay')) ?? false;
|
||||||
_showHikingTrailsOverlay = prefs.getBool('map_show_hiking_trails_overlay') ?? false;
|
_showForestRoadsOverlay =
|
||||||
_showMainRoadsOverlay = prefs.getBool('map_show_main_roads_overlay') ?? false;
|
prefs.getBool(_scopedKey('map_show_forest_roads_overlay')) ?? false;
|
||||||
_showHouseNumbersOverlay = prefs.getBool('map_show_house_numbers_overlay') ?? false;
|
_showHikingTrailsOverlay =
|
||||||
_showFireHazardZonesOverlay = prefs.getBool('map_show_fire_hazard_zones_overlay') ?? false;
|
prefs.getBool(_scopedKey('map_show_hiking_trails_overlay')) ?? false;
|
||||||
_showHistoricalFiresOverlay = prefs.getBool('map_show_historical_fires_overlay') ?? false;
|
_showMainRoadsOverlay =
|
||||||
_showFirebreaksOverlay = prefs.getBool('map_show_firebreaks_overlay') ?? false;
|
prefs.getBool(_scopedKey('map_show_main_roads_overlay')) ?? false;
|
||||||
_showKrasFireZonesOverlay = prefs.getBool('map_show_kras_fire_zones_overlay') ?? false;
|
_showHouseNumbersOverlay =
|
||||||
_showPlaceNamesOverlay = prefs.getBool('map_show_place_names_overlay') ?? false;
|
prefs.getBool(_scopedKey('map_show_house_numbers_overlay')) ?? false;
|
||||||
_showMunicipalityBordersOverlay = prefs.getBool('map_show_municipality_borders_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();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _loadInitialState() async {
|
Future<void> _loadInitialState() async {
|
||||||
await Future.wait([loadOverlayState(), loadTrailSettings()]);
|
await Future.wait([
|
||||||
await loadRepeaterVisibilitySettings();
|
loadOverlayState(),
|
||||||
|
loadRepeaterVisibilitySettings(),
|
||||||
|
_loadCustomMapState(),
|
||||||
|
_removeDeprecatedSettings(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _removeDeprecatedSettings() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.remove(_scopedKey('map_show_all_contact_trails'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Save overlay state to SharedPreferences
|
|
||||||
Future<void> _saveOverlayState() async {
|
Future<void> _saveOverlayState() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool('map_show_cadastral_overlay', _showCadastralOverlay);
|
await prefs.setBool(
|
||||||
await prefs.setBool('map_show_forest_roads_overlay', _showForestRoadsOverlay);
|
_scopedKey('map_show_cadastral_overlay'),
|
||||||
await prefs.setBool('map_show_hiking_trails_overlay', _showHikingTrailsOverlay);
|
_showCadastralOverlay,
|
||||||
await prefs.setBool('map_show_main_roads_overlay', _showMainRoadsOverlay);
|
);
|
||||||
await prefs.setBool('map_show_house_numbers_overlay', _showHouseNumbersOverlay);
|
await prefs.setBool(
|
||||||
await prefs.setBool('map_show_fire_hazard_zones_overlay', _showFireHazardZonesOverlay);
|
_scopedKey('map_show_forest_roads_overlay'),
|
||||||
await prefs.setBool('map_show_historical_fires_overlay', _showHistoricalFiresOverlay);
|
_showForestRoadsOverlay,
|
||||||
await prefs.setBool('map_show_firebreaks_overlay', _showFirebreaksOverlay);
|
);
|
||||||
await prefs.setBool('map_show_kras_fire_zones_overlay', _showKrasFireZonesOverlay);
|
await prefs.setBool(
|
||||||
await prefs.setBool('map_show_place_names_overlay', _showPlaceNamesOverlay);
|
_scopedKey('map_show_hiking_trails_overlay'),
|
||||||
await prefs.setBool('map_show_municipality_borders_overlay', _showMunicipalityBordersOverlay);
|
_showHikingTrailsOverlay,
|
||||||
}
|
);
|
||||||
|
await prefs.setBool(
|
||||||
/// Toggle all contact trails on/off
|
_scopedKey('map_show_main_roads_overlay'),
|
||||||
Future<void> toggleAllContactTrails() async {
|
_showMainRoadsOverlay,
|
||||||
_showAllContactTrails = !_showAllContactTrails;
|
);
|
||||||
notifyListeners();
|
await prefs.setBool(
|
||||||
await _saveTrailSettings();
|
_scopedKey('map_show_house_numbers_overlay'),
|
||||||
}
|
_showHouseNumbersOverlay,
|
||||||
|
);
|
||||||
/// Load trail settings from SharedPreferences
|
await prefs.setBool(
|
||||||
Future<void> loadTrailSettings() async {
|
_scopedKey('map_show_fire_hazard_zones_overlay'),
|
||||||
final prefs = await SharedPreferences.getInstance();
|
_showFireHazardZonesOverlay,
|
||||||
_showAllContactTrails = prefs.getBool('map_show_all_contact_trails') ?? true; // Default to true (show all)
|
);
|
||||||
notifyListeners();
|
await prefs.setBool(
|
||||||
}
|
_scopedKey('map_show_historical_fires_overlay'),
|
||||||
|
_showHistoricalFiresOverlay,
|
||||||
/// Save trail settings to SharedPreferences
|
);
|
||||||
Future<void> _saveTrailSettings() async {
|
await prefs.setBool(
|
||||||
final prefs = await SharedPreferences.getInstance();
|
_scopedKey('map_show_firebreaks_overlay'),
|
||||||
await prefs.setBool('map_show_all_contact_trails', _showAllContactTrails);
|
_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,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> setHideRepeatersOnMap(bool hide) async {
|
Future<void> setHideRepeatersOnMap(bool hide) async {
|
||||||
@@ -404,57 +590,166 @@ class MapProvider with ChangeNotifier {
|
|||||||
_hideRepeatersOnMap = hide;
|
_hideRepeatersOnMap = hide;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool('map_hide_repeaters', _hideRepeatersOnMap);
|
await prefs.setBool(_scopedKey('map_hide_repeaters'), _hideRepeatersOnMap);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> loadRepeaterVisibilitySettings() async {
|
Future<void> loadRepeaterVisibilitySettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
_hideRepeatersOnMap = prefs.getBool('map_hide_repeaters') ?? false;
|
_hideRepeatersOnMap =
|
||||||
|
prefs.getBool(_scopedKey('map_hide_repeaters')) ?? false;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set imported trail (from GPX import)
|
|
||||||
void setImportedTrail(LocationTrail trail) {
|
void setImportedTrail(LocationTrail trail) {
|
||||||
_importedTrail = trail;
|
_importedTrail = trail;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear imported trail
|
|
||||||
void clearImportedTrail() {
|
void clearImportedTrail() {
|
||||||
_importedTrail = null;
|
_importedTrail = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace current trail with imported trail
|
|
||||||
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
void replaceCurrentTrailWithImport(LocationTrail importedTrail) {
|
||||||
// End current trail if active
|
|
||||||
if (_currentTrail != null && _currentTrail!.isActive) {
|
if (_currentTrail != null && _currentTrail!.isActive) {
|
||||||
endTrail();
|
endTrail();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set imported trail as current trail
|
|
||||||
_currentTrail = importedTrail;
|
_currentTrail = importedTrail;
|
||||||
_isTrailVisible = true;
|
_isTrailVisible = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Enter download area selection mode with initial bounds
|
|
||||||
void enterDownloadAreaMode(LatLngBounds initialBounds) {
|
void enterDownloadAreaMode(LatLngBounds initialBounds) {
|
||||||
_isSelectingDownloadArea = true;
|
_isSelectingDownloadArea = true;
|
||||||
_downloadAreaBounds = initialBounds;
|
_downloadAreaBounds = initialBounds;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Exit download area selection mode
|
|
||||||
void exitDownloadAreaMode() {
|
void exitDownloadAreaMode() {
|
||||||
_isSelectingDownloadArea = false;
|
_isSelectingDownloadArea = false;
|
||||||
_downloadAreaBounds = null;
|
_downloadAreaBounds = null;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the download area bounds (while dragging/resizing)
|
|
||||||
void updateDownloadAreaBounds(LatLngBounds bounds) {
|
void updateDownloadAreaBounds(LatLngBounds bounds) {
|
||||||
_downloadAreaBounds = bounds;
|
_downloadAreaBounds = bounds;
|
||||||
notifyListeners();
|
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,
|
||||||
|
'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;
|
||||||
|
_hideRepeatersOnMap = json['hideRepeatersOnMap'] as bool? ?? false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
String _scopedKey(String baseKey) {
|
||||||
|
return ProfileStorageScope.scopedKey(baseKey);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,8 @@ import '../utils/image_message_parser.dart';
|
|||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
import 'helpers/message_retry_manager.dart';
|
import 'helpers/message_retry_manager.dart';
|
||||||
|
|
||||||
|
typedef DisplayMessageEntry = ({Message message, int occurrenceCount});
|
||||||
|
|
||||||
/// Messages Provider - manages message history and SAR markers
|
/// Messages Provider - manages message history and SAR markers
|
||||||
class MessagesProvider with ChangeNotifier {
|
class MessagesProvider with ChangeNotifier {
|
||||||
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
|
static const Duration _channelEchoWarningDelay = Duration(seconds: 12);
|
||||||
@@ -28,11 +30,14 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final MessageStorageService _storageService = MessageStorageService();
|
final MessageStorageService _storageService = MessageStorageService();
|
||||||
final NotificationService _notificationService = NotificationService();
|
final NotificationService _notificationService = NotificationService();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
|
bool _isPersisting = false;
|
||||||
|
bool _persistRequested = false;
|
||||||
AppLocalizations? _localizations;
|
AppLocalizations? _localizations;
|
||||||
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
final Map<String, MessageContactLocation> _messageContactLocations = {};
|
||||||
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
final Map<String, MessageReceptionDetails> _messageReceptionDetails = {};
|
||||||
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
final Map<String, MessageTransferDetails> _messageTransferDetails = {};
|
||||||
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
final Map<String, MessageRouteMetadata> _messageRouteMetadata = {};
|
||||||
|
String? _storageNamespace;
|
||||||
|
|
||||||
// Track pending sent messages by expected ACK/TAG
|
// Track pending sent messages by expected ACK/TAG
|
||||||
final Map<int, Message> _pendingSentMessages = {};
|
final Map<int, Message> _pendingSentMessages = {};
|
||||||
@@ -91,6 +96,10 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> Function({required Contact contact, required int failureStreak})?
|
Future<void> Function({required Contact contact, required int failureStreak})?
|
||||||
onDirectPathFailedCallback;
|
onDirectPathFailedCallback;
|
||||||
|
|
||||||
|
/// Called before the last retry attempt to reset the contact's path,
|
||||||
|
/// forcing the firmware to use flood mode for the final try.
|
||||||
|
Future<void> Function(Contact contact)? resetPathBeforeLastRetryCallback;
|
||||||
void Function(String messageId)? onManualRetryPreparedCallback;
|
void Function(String messageId)? onManualRetryPreparedCallback;
|
||||||
Future<bool> Function({
|
Future<bool> Function({
|
||||||
required String messageId,
|
required String messageId,
|
||||||
@@ -145,6 +154,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
sarMarkers.where((m) => m.type == SarMarkerType.object).toList();
|
sarMarkers.where((m) => m.type == SarMarkerType.object).toList();
|
||||||
|
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
String? get storageNamespace => _storageNamespace;
|
||||||
|
|
||||||
String? get targetMessageId => _targetMessageId;
|
String? get targetMessageId => _targetMessageId;
|
||||||
|
|
||||||
@@ -307,19 +317,64 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
await _loadFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reloadFromStorage({String? namespace}) async {
|
||||||
|
_storageNamespace = namespace;
|
||||||
|
await _loadFromStorage(force: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> persistNow() async {
|
||||||
|
await _storageService.saveMessages(
|
||||||
|
_messages,
|
||||||
|
messageContactLocations: _messageContactLocations,
|
||||||
|
messageReceptionDetails: _messageReceptionDetails,
|
||||||
|
messageTransferDetails: _messageTransferDetails,
|
||||||
|
messageRouteMetadata: _messageRouteMetadata,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
await _storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cloneCurrentStorageTo(String? namespace) async {
|
||||||
|
await _storageService.saveMessages(
|
||||||
|
_messages,
|
||||||
|
messageContactLocations: _messageContactLocations,
|
||||||
|
messageReceptionDetails: _messageReceptionDetails,
|
||||||
|
messageTransferDetails: _messageTransferDetails,
|
||||||
|
messageRouteMetadata: _messageRouteMetadata,
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
|
await _storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFromStorage({bool force = false}) async {
|
||||||
|
if (_isInitialized && !force) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_cancelAllTimers();
|
||||||
|
_resetInMemoryState();
|
||||||
debugPrint('📦 [MessagesProvider] Loading persisted messages...');
|
debugPrint('📦 [MessagesProvider] Loading persisted messages...');
|
||||||
final storedMessages = await _storageService.loadMessages();
|
final storedMessages = await _storageService.loadMessages(
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
final storedContactLocations = await _storageService
|
final storedContactLocations = await _storageService
|
||||||
.loadMessageContactLocations();
|
.loadMessageContactLocations(namespace: _storageNamespace);
|
||||||
final storedReceptionDetails = await _storageService
|
final storedReceptionDetails = await _storageService
|
||||||
.loadMessageReceptionDetails();
|
.loadMessageReceptionDetails(namespace: _storageNamespace);
|
||||||
final storedTransferDetails = await _storageService
|
final storedTransferDetails = await _storageService
|
||||||
.loadMessageTransferDetails();
|
.loadMessageTransferDetails(namespace: _storageNamespace);
|
||||||
final storedRouteMetadata = await _storageService
|
final storedRouteMetadata = await _storageService
|
||||||
.loadMessageRouteMetadata();
|
.loadMessageRouteMetadata(namespace: _storageNamespace);
|
||||||
final storedRemovedSarMarkerIds = await _storageService
|
final storedRemovedSarMarkerIds = await _storageService
|
||||||
.loadRemovedSarMarkerIds();
|
.loadRemovedSarMarkerIds(namespace: _storageNamespace);
|
||||||
_messageContactLocations
|
_messageContactLocations
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(storedContactLocations);
|
..addAll(storedContactLocations);
|
||||||
@@ -549,6 +604,15 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final incomingPathBytes = receptionDetailsSnapshot?.pathBytes;
|
||||||
|
if (incomingPathBytes != null &&
|
||||||
|
incomingPathBytes.isNotEmpty &&
|
||||||
|
finalMessage.pathBytes == null) {
|
||||||
|
finalMessage = finalMessage.copyWith(
|
||||||
|
pathBytes: Uint8List.fromList(incomingPathBytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Debug: Check if message is SAR
|
// Debug: Check if message is SAR
|
||||||
if (message.text.startsWith('S:')) {
|
if (message.text.startsWith('S:')) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -580,8 +644,21 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
if (contactLocationSnapshot != null) {
|
if (contactLocationSnapshot != null) {
|
||||||
_messageContactLocations[existingId] = contactLocationSnapshot;
|
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||||
}
|
}
|
||||||
|
_messageReceptionDetails[existingId] =
|
||||||
|
MessageReceptionDetails.mergeDuplicate(
|
||||||
|
existing: _messageReceptionDetails[existingId],
|
||||||
|
incoming: receptionDetailsSnapshot,
|
||||||
|
);
|
||||||
if (receptionDetailsSnapshot != null) {
|
if (receptionDetailsSnapshot != null) {
|
||||||
_messageReceptionDetails[existingId] = receptionDetailsSnapshot;
|
final existingMessage = _messages[duplicateIndex];
|
||||||
|
final duplicatePathBytes = receptionDetailsSnapshot.pathBytes;
|
||||||
|
if (existingMessage.pathBytes == null &&
|
||||||
|
duplicatePathBytes != null &&
|
||||||
|
duplicatePathBytes.isNotEmpty) {
|
||||||
|
_messages[duplicateIndex] = existingMessage.copyWith(
|
||||||
|
pathBytes: Uint8List.fromList(duplicatePathBytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
return; // Skip duplicate
|
return; // Skip duplicate
|
||||||
@@ -658,7 +735,17 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (message.isContactMessage) {
|
if (message.isContactMessage) {
|
||||||
return existing.senderKeyShort == message.senderKeyShort;
|
// Match by sender key + sender timestamp (matches official app's DB
|
||||||
|
// uniqueness: contactPublicKey + senderTimestamp + text + txtType).
|
||||||
|
if (existing.senderKeyShort == message.senderKeyShort &&
|
||||||
|
existing.senderTimestamp == message.senderTimestamp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback dedup when retransmits surface as separate inbound rows
|
||||||
|
// without a stable timestamp/message id, but still carry the same
|
||||||
|
// visible sender identity and payload.
|
||||||
|
return _matchesDuplicateSenderIdentity(existing, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.isChannelMessage) {
|
if (message.isChannelMessage) {
|
||||||
@@ -666,56 +753,41 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Primary dedup: same senderTimestamp = same message from mesh repeats.
|
||||||
|
// Matches official app's DB uniqueness: (channelSecret, senderTimestamp, text).
|
||||||
|
if (existing.senderTimestamp == message.senderTimestamp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Secondary: catch near-duplicate repeats within a 30s window
|
||||||
|
// (clock drift between nodes).
|
||||||
final withinChannelRepeatWindow =
|
final withinChannelRepeatWindow =
|
||||||
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
||||||
|
if (!withinChannelRepeatWindow) {
|
||||||
final existingSenderKey = existing.senderKeyShort;
|
return false;
|
||||||
final incomingSenderKey = message.senderKeyShort;
|
|
||||||
if (existingSenderKey != null &&
|
|
||||||
incomingSenderKey != null &&
|
|
||||||
existingSenderKey == incomingSenderKey &&
|
|
||||||
withinChannelRepeatWindow) {
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
final existingSenderName = _normalizedResolvedSenderName(existing);
|
return _matchesDuplicateSenderIdentity(existing, message);
|
||||||
final incomingSenderName = _normalizedResolvedSenderName(message);
|
|
||||||
if (existing.isChannelMessage && message.isChannelMessage) {
|
|
||||||
debugPrint(
|
|
||||||
'🧪 [MessagesProvider] Channel dedupe check: '
|
|
||||||
'existing(id=${existing.id}, sent=${existing.isSentMessage}, senderName=${existing.senderName ?? "-"}, resolvedSender=${existingSenderName ?? "-"}, senderKey=${existing.senderKeyShort ?? "-"}, ts=${existing.senderTimestamp}, pathLen=${existing.pathLen}) '
|
|
||||||
'incoming(id=${message.id}, sent=${message.isSentMessage}, senderName=${message.senderName ?? "-"}, resolvedSender=${incomingSenderName ?? "-"}, senderKey=${message.senderKeyShort ?? "-"}, ts=${message.senderTimestamp}, pathLen=${message.pathLen}) '
|
|
||||||
'withinWindow=$withinChannelRepeatWindow',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (existingSenderName != null &&
|
|
||||||
incomingSenderName != null &&
|
|
||||||
existingSenderName == incomingSenderName) {
|
|
||||||
if (!existing.isSentMessage && !message.isSentMessage) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!withinChannelRepeatWindow) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mirror meshcore-open behavior for self-authored channel messages:
|
|
||||||
// zero-hop self replays are dropped earlier in AppProvider, while
|
|
||||||
// routed self replays should fold into the local sent bubble when the
|
|
||||||
// public sender name matches within a short window.
|
|
||||||
if (existing.isSentMessage &&
|
|
||||||
!message.isSentMessage &&
|
|
||||||
existingSenderName == incomingSenderName) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
// System messages and other types: never deduplicate by scope alone.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesDuplicateSenderIdentity(Message existing, Message message) {
|
||||||
|
final existingSenderKey = existing.senderKeyShort;
|
||||||
|
final incomingSenderKey = message.senderKeyShort;
|
||||||
|
if (existingSenderKey != null &&
|
||||||
|
incomingSenderKey != null &&
|
||||||
|
existingSenderKey == incomingSenderKey) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingSenderName = _normalizedResolvedSenderName(existing);
|
||||||
|
final incomingSenderName = _normalizedResolvedSenderName(message);
|
||||||
|
return existingSenderName != null &&
|
||||||
|
incomingSenderName != null &&
|
||||||
|
existingSenderName == incomingSenderName;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add multiple messages
|
/// Add multiple messages
|
||||||
@@ -725,10 +797,42 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
// Always enhance message with SAR parser to detect SAR markers
|
// Always enhance message with SAR parser to detect SAR markers
|
||||||
final enhancedMessage = SarMessageParser.enhanceMessage(message);
|
var enhancedMessage = SarMessageParser.enhanceMessage(message);
|
||||||
|
|
||||||
|
// Check if it's a drawing message (D:...) and not already marked
|
||||||
|
if (DrawingMessageParser.isDrawingMessage(enhancedMessage.text) &&
|
||||||
|
!enhancedMessage.isDrawing) {
|
||||||
|
final drawing = DrawingMessageParser.parseDrawingMessage(
|
||||||
|
enhancedMessage.text,
|
||||||
|
senderName: enhancedMessage.senderName,
|
||||||
|
messageId: enhancedMessage.id,
|
||||||
|
);
|
||||||
|
enhancedMessage = enhancedMessage.copyWith(
|
||||||
|
isDrawing: true,
|
||||||
|
drawingId: drawing?.id,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's a voice message and not already marked
|
||||||
|
if (!enhancedMessage.isVoice) {
|
||||||
|
final envelope = VoiceEnvelope.tryParseText(enhancedMessage.text);
|
||||||
|
if (envelope != null) {
|
||||||
|
enhancedMessage = enhancedMessage.copyWith(
|
||||||
|
isVoice: true,
|
||||||
|
voiceId: envelope.sessionId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||||
|
|
||||||
// Check for duplicates
|
// Check for duplicates
|
||||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
final duplicateIndex = _findDuplicateMessageIndex(enhancedMessage);
|
||||||
|
if (duplicateIndex != -1) {
|
||||||
|
final existingId = _messages[duplicateIndex].id;
|
||||||
|
_messageReceptionDetails[existingId] =
|
||||||
|
MessageReceptionDetails.mergeDuplicate(
|
||||||
|
existing: _messageReceptionDetails[existingId],
|
||||||
|
);
|
||||||
duplicateCount++;
|
duplicateCount++;
|
||||||
continue; // Skip duplicate
|
continue; // Skip duplicate
|
||||||
}
|
}
|
||||||
@@ -973,18 +1077,32 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return '${mib.toStringAsFixed(mib >= 10 ? 0 : 1)} MB';
|
return '${mib.toStringAsFixed(mib >= 10 ? 0 : 1)} MB';
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Persist messages to storage (async, non-blocking)
|
/// Persist messages to storage (async, non-blocking, coalescing).
|
||||||
|
///
|
||||||
|
/// Multiple rapid calls are coalesced into a single write to avoid
|
||||||
|
/// overlapping serialization and redundant SharedPreferences writes.
|
||||||
Future<void> _persistMessages() async {
|
Future<void> _persistMessages() async {
|
||||||
|
_persistRequested = true;
|
||||||
|
if (_isPersisting) {
|
||||||
|
return; // A write is in flight; it will pick up our changes.
|
||||||
|
}
|
||||||
|
_isPersisting = true;
|
||||||
try {
|
try {
|
||||||
await _storageService.saveMessages(
|
while (_persistRequested) {
|
||||||
_messages,
|
_persistRequested = false;
|
||||||
messageContactLocations: _messageContactLocations,
|
await _storageService.saveMessages(
|
||||||
messageReceptionDetails: _messageReceptionDetails,
|
_messages,
|
||||||
messageTransferDetails: _messageTransferDetails,
|
messageContactLocations: _messageContactLocations,
|
||||||
messageRouteMetadata: _messageRouteMetadata,
|
messageReceptionDetails: _messageReceptionDetails,
|
||||||
);
|
messageTransferDetails: _messageTransferDetails,
|
||||||
|
messageRouteMetadata: _messageRouteMetadata,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
debugPrint('❌ [MessagesProvider] Error persisting messages: $e');
|
||||||
|
} finally {
|
||||||
|
_isPersisting = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1028,6 +1146,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
for (final messageId in messageIdsToRemove) {
|
for (final messageId in messageIdsToRemove) {
|
||||||
_timeoutTimers[messageId]?.cancel();
|
_timeoutTimers[messageId]?.cancel();
|
||||||
_timeoutTimers.remove(messageId);
|
_timeoutTimers.remove(messageId);
|
||||||
|
_clearChannelSendWarning(messageId);
|
||||||
_messageContactMap.remove(messageId);
|
_messageContactMap.remove(messageId);
|
||||||
_groupedMessageMapping.remove(messageId);
|
_groupedMessageMapping.remove(messageId);
|
||||||
_messageContactLocations.remove(messageId);
|
_messageContactLocations.remove(messageId);
|
||||||
@@ -1062,6 +1181,35 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return sorted.take(count).toList();
|
return sorted.take(count).toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<DisplayMessageEntry> buildDisplayMessages(Iterable<Message> messages) {
|
||||||
|
final entries = <DisplayMessageEntry>[];
|
||||||
|
|
||||||
|
for (final message in messages) {
|
||||||
|
final occurrenceCount = _messageOccurrenceCount(message);
|
||||||
|
final existingIndex = entries.indexWhere(
|
||||||
|
(entry) =>
|
||||||
|
entry.message.text == message.text &&
|
||||||
|
_matchesDuplicateScope(entry.message, message),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex == -1) {
|
||||||
|
entries.add((message: message, occurrenceCount: occurrenceCount));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingEntry = entries[existingIndex];
|
||||||
|
entries[existingIndex] = (
|
||||||
|
message: existingEntry.message,
|
||||||
|
occurrenceCount: existingEntry.occurrenceCount + occurrenceCount,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _messageOccurrenceCount(Message message) =>
|
||||||
|
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||||
|
|
||||||
/// Get messages from last N hours
|
/// Get messages from last N hours
|
||||||
List<Message> getMessagesSince(Duration duration) {
|
List<Message> getMessagesSince(Duration duration) {
|
||||||
final cutoff = DateTime.now().subtract(duration);
|
final cutoff = DateTime.now().subtract(duration);
|
||||||
@@ -1097,7 +1245,10 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
Future<void> removeSarMarker(String id) async {
|
Future<void> removeSarMarker(String id) async {
|
||||||
_sarMarkers.remove(id);
|
_sarMarkers.remove(id);
|
||||||
_removedSarMarkerIds.add(id);
|
_removedSarMarkerIds.add(id);
|
||||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
await _storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1222,7 +1373,12 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
debugPrint('🗑️ [MessagesProvider] Message $messageId deleted');
|
||||||
|
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
unawaited(
|
||||||
|
_storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1245,20 +1401,16 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Clear all messages
|
/// Clear all messages
|
||||||
void clearMessages() {
|
void clearMessages() {
|
||||||
for (final timer in _channelEchoWarningTimers.values) {
|
_cancelAllTimers();
|
||||||
timer.cancel();
|
_resetInMemoryState();
|
||||||
}
|
_retryManager.clearAll();
|
||||||
_channelEchoWarningTimers.clear();
|
|
||||||
_channelEchoWarningMessageIds.clear();
|
|
||||||
_messages.clear();
|
|
||||||
_sarMarkers.clear();
|
|
||||||
_removedSarMarkerIds.clear();
|
|
||||||
_messageContactLocations.clear();
|
|
||||||
_messageReceptionDetails.clear();
|
|
||||||
_messageTransferDetails.clear();
|
|
||||||
_messageRouteMetadata.clear();
|
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
unawaited(
|
||||||
|
_storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1266,17 +1418,29 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
Future<void> clearSarMarkers() async {
|
Future<void> clearSarMarkers() async {
|
||||||
_removedSarMarkerIds.addAll(_sarMarkers.keys);
|
_removedSarMarkerIds.addAll(_sarMarkers.keys);
|
||||||
_sarMarkers.clear();
|
_sarMarkers.clear();
|
||||||
await _storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds);
|
await _storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all data
|
/// Clear all data
|
||||||
void clearAll() {
|
void clearAll() {
|
||||||
for (final timer in _channelEchoWarningTimers.values) {
|
_cancelAllTimers();
|
||||||
timer.cancel();
|
_resetInMemoryState();
|
||||||
}
|
_retryManager.clearAll();
|
||||||
_channelEchoWarningTimers.clear();
|
_persistMessages();
|
||||||
_channelEchoWarningMessageIds.clear();
|
unawaited(
|
||||||
|
_storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetInMemoryState() {
|
||||||
_messages.clear();
|
_messages.clear();
|
||||||
_sarMarkers.clear();
|
_sarMarkers.clear();
|
||||||
_removedSarMarkerIds.clear();
|
_removedSarMarkerIds.clear();
|
||||||
@@ -1284,9 +1448,13 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageReceptionDetails.clear();
|
_messageReceptionDetails.clear();
|
||||||
_messageTransferDetails.clear();
|
_messageTransferDetails.clear();
|
||||||
_messageRouteMetadata.clear();
|
_messageRouteMetadata.clear();
|
||||||
_persistMessages();
|
_pendingSentMessages.clear();
|
||||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
_messageContactMap.clear();
|
||||||
notifyListeners();
|
_groupedMessageMapping.clear();
|
||||||
|
_ackTagToRecipients.clear();
|
||||||
|
_messageAckHistory.clear();
|
||||||
|
_ackHistoryLookup.clear();
|
||||||
|
_completedAckHistory.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
int transferCountForSession({
|
int transferCountForSession({
|
||||||
@@ -1964,7 +2132,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
debugPrint(' Message index in list: $index');
|
debugPrint(' Message index in list: $index');
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = _messages[index].copyWith(
|
||||||
deliveryStatus: MessageDeliveryStatus.delivered,
|
deliveryStatus: MessageDeliveryStatus.delivered,
|
||||||
roundTripTimeMs: roundTripTimeMs,
|
roundTripTimeMs: roundTripTimeMs,
|
||||||
deliveredAt: DateTime.now(),
|
deliveredAt: DateTime.now(),
|
||||||
@@ -2146,7 +2314,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
final delayMs = _retryManager.getDelayForAttempt(message.retryAttempt);
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttempts} for message $messageId',
|
'🔄 [MessagesProvider] Scheduling retry $nextAttempt/${MessageRetryManager.maxRetryAttemptsForContact(contact)} for message $messageId',
|
||||||
);
|
);
|
||||||
debugPrint(' Delay: ${delayMs}ms');
|
debugPrint(' Delay: ${delayMs}ms');
|
||||||
|
|
||||||
@@ -2186,6 +2354,17 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On the last attempt, reset the path to force flood mode
|
||||||
|
// (matches official MeshCore app behaviour)
|
||||||
|
if (_retryManager.isLastAttempt(currentMessage, contact)) {
|
||||||
|
debugPrint(
|
||||||
|
'🔄 [MessagesProvider] Last attempt — resetting path to flood for $messageId',
|
||||||
|
);
|
||||||
|
if (resetPathBeforeLastRetryCallback != null) {
|
||||||
|
await resetPathBeforeLastRetryCallback!(contact);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sendMessageCallback != null) {
|
if (sendMessageCallback != null) {
|
||||||
final queued = await sendMessageCallback!(
|
final queued = await sendMessageCallback!(
|
||||||
contactPublicKey: contact.publicKey,
|
contactPublicKey: contact.publicKey,
|
||||||
@@ -2323,11 +2502,14 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
senderPublicKeyPrefix: message.senderPublicKeyPrefix,
|
senderPublicKeyPrefix: message.senderPublicKeyPrefix,
|
||||||
channelIdx: message.channelIdx,
|
channelIdx: message.channelIdx,
|
||||||
pathLen: message.pathLen,
|
pathLen: message.pathLen,
|
||||||
|
pathBytes: message.pathBytes,
|
||||||
textType: message.textType,
|
textType: message.textType,
|
||||||
senderTimestamp: message.senderTimestamp,
|
senderTimestamp: message.senderTimestamp,
|
||||||
text: message.text,
|
text: message.text,
|
||||||
isSarMarker: message.isSarMarker,
|
isSarMarker: message.isSarMarker,
|
||||||
sarGpsCoordinates: message.sarGpsCoordinates,
|
sarGpsCoordinates: message.sarGpsCoordinates,
|
||||||
|
sarCustomMapPoint: message.sarCustomMapPoint,
|
||||||
|
sarCustomMapId: message.sarCustomMapId,
|
||||||
sarNotes: message.sarNotes,
|
sarNotes: message.sarNotes,
|
||||||
sarCustomEmoji: message.sarCustomEmoji,
|
sarCustomEmoji: message.sarCustomEmoji,
|
||||||
sarColorIndex: message.sarColorIndex,
|
sarColorIndex: message.sarColorIndex,
|
||||||
@@ -2410,7 +2592,19 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
// Cancel all pending timeout timers
|
_cancelAllTimers();
|
||||||
|
_completedAckHistory.clear();
|
||||||
|
_messageAckHistory.clear();
|
||||||
|
_ackHistoryLookup.clear();
|
||||||
|
|
||||||
|
// Clear retry manager
|
||||||
|
_retryManager.clearAll();
|
||||||
|
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cancel all pending timers (timeout + echo warning).
|
||||||
|
void _cancelAllTimers() {
|
||||||
for (final timer in _timeoutTimers.values) {
|
for (final timer in _timeoutTimers.values) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
}
|
}
|
||||||
@@ -2420,14 +2614,6 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
_channelEchoWarningTimers.clear();
|
_channelEchoWarningTimers.clear();
|
||||||
_channelEchoWarningMessageIds.clear();
|
_channelEchoWarningMessageIds.clear();
|
||||||
_completedAckHistory.clear();
|
|
||||||
_messageAckHistory.clear();
|
|
||||||
_ackHistoryLookup.clear();
|
|
||||||
|
|
||||||
// Clear retry manager
|
|
||||||
_retryManager.clearAll();
|
|
||||||
|
|
||||||
super.dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _rememberCompletedAck(int ackCode) {
|
void _rememberCompletedAck(int ackCode) {
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../services/profiles_feature_service.dart';
|
||||||
|
import '../widgets/sensors/sensor_telemetry_card.dart';
|
||||||
import 'connection_provider.dart';
|
import 'connection_provider.dart';
|
||||||
import 'contacts_provider.dart';
|
import 'contacts_provider.dart';
|
||||||
|
|
||||||
@@ -12,9 +14,23 @@ enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
|||||||
|
|
||||||
class SensorsProvider with ChangeNotifier {
|
class SensorsProvider with ChangeNotifier {
|
||||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||||
|
static const int selfAutoRefreshMinutes = 1;
|
||||||
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
static const String _watchedSensorsKey = 'watched_sensor_keys';
|
||||||
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
static const String _visibleSensorMetricsKey = 'visible_sensor_metrics';
|
||||||
static const String _fieldSpanKey = 'sensor_field_spans';
|
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>{
|
static const Set<String> _defaultVisibleFields = <String>{
|
||||||
'voltage',
|
'voltage',
|
||||||
'battery',
|
'battery',
|
||||||
@@ -23,6 +39,14 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
'pressure',
|
'pressure',
|
||||||
'gps',
|
'gps',
|
||||||
};
|
};
|
||||||
|
static const List<String> _defaultMetricOrder = <String>[
|
||||||
|
'voltage',
|
||||||
|
'battery',
|
||||||
|
'temperature',
|
||||||
|
'humidity',
|
||||||
|
'pressure',
|
||||||
|
'gps',
|
||||||
|
];
|
||||||
|
|
||||||
final List<String> _watchedSensorKeys = <String>[];
|
final List<String> _watchedSensorKeys = <String>[];
|
||||||
final Map<String, SensorRefreshState> _refreshStates =
|
final Map<String, SensorRefreshState> _refreshStates =
|
||||||
@@ -32,13 +56,22 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
<String, Set<String>>{};
|
<String, Set<String>>{};
|
||||||
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
final Map<String, Map<String, int>> _fieldSpansBySensor =
|
||||||
<String, Map<String, int>>{};
|
<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 _isLoaded = false;
|
||||||
bool _isRefreshingAll = false;
|
bool _isRefreshingAll = false;
|
||||||
|
bool _isRunningAutoRefreshTick = false;
|
||||||
|
|
||||||
SensorsProvider() {
|
SensorsProvider() {
|
||||||
unawaited(_loadWatchedSensors());
|
unawaited(_loadWatchedSensors());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _key(String baseKey) => ProfileStorageScope.scopedKey(baseKey);
|
||||||
|
|
||||||
List<String> get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys);
|
List<String> get watchedSensorKeys => List.unmodifiable(_watchedSensorKeys);
|
||||||
bool get isLoaded => _isLoaded;
|
bool get isLoaded => _isLoaded;
|
||||||
bool get isRefreshingAll => _isRefreshingAll;
|
bool get isRefreshingAll => _isRefreshingAll;
|
||||||
@@ -49,14 +82,26 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
Future<void> _loadWatchedSensors() async {
|
Future<void> _loadWatchedSensors() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final stored = prefs.getStringList(_watchedSensorsKey) ?? <String>[];
|
final stored =
|
||||||
final storedMetricsJson = prefs.getString(_visibleSensorMetricsKey);
|
prefs.getStringList(_key(_watchedSensorsKey)) ?? <String>[];
|
||||||
final storedSpansJson = prefs.getString(_fieldSpanKey);
|
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
|
_watchedSensorKeys
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(stored);
|
..addAll(stored);
|
||||||
|
_refreshStates.clear();
|
||||||
|
_refreshStateUpdatedAt.clear();
|
||||||
_visibleFieldsBySensor.clear();
|
_visibleFieldsBySensor.clear();
|
||||||
_fieldSpansBySensor.clear();
|
_fieldSpansBySensor.clear();
|
||||||
|
_metricLabelsBySensor.clear();
|
||||||
|
_metricOrderBySensor.clear();
|
||||||
|
_autoRefreshMinutesBySensor.clear();
|
||||||
|
_lastRefreshAttemptAt.clear();
|
||||||
if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) {
|
if (storedMetricsJson != null && storedMetricsJson.isNotEmpty) {
|
||||||
final decoded = jsonDecode(storedMetricsJson) as Map<String, dynamic>;
|
final decoded = jsonDecode(storedMetricsJson) as Map<String, dynamic>;
|
||||||
for (final entry in decoded.entries) {
|
for (final entry in decoded.entries) {
|
||||||
@@ -72,12 +117,49 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
.map((key, value) => MapEntry(key, value as int));
|
.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) {
|
for (final key in _watchedSensorKeys) {
|
||||||
_visibleFieldsBySensor.putIfAbsent(
|
_visibleFieldsBySensor.putIfAbsent(
|
||||||
key,
|
key,
|
||||||
() => Set<String>.from(_defaultVisibleFields),
|
() => Set<String>.from(_defaultVisibleFields),
|
||||||
);
|
);
|
||||||
_fieldSpansBySensor.putIfAbsent(key, () => <String, int>{});
|
_fieldSpansBySensor.putIfAbsent(key, () => <String, int>{});
|
||||||
|
_metricLabelsBySensor.putIfAbsent(key, () => <String, String>{});
|
||||||
|
_metricOrderBySensor.putIfAbsent(
|
||||||
|
key,
|
||||||
|
() => List<String>.from(_defaultMetricOrder),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error loading watched sensors: $e');
|
debugPrint('Error loading watched sensors: $e');
|
||||||
@@ -87,10 +169,15 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reloadProfileScopedState() async {
|
||||||
|
_isLoaded = false;
|
||||||
|
await _loadWatchedSensors();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistWatchedSensors() async {
|
Future<void> _persistWatchedSensors() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setStringList(_watchedSensorsKey, _watchedSensorKeys);
|
await prefs.setStringList(_key(_watchedSensorsKey), _watchedSensorKeys);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error saving watched sensors: $e');
|
debugPrint('Error saving watched sensors: $e');
|
||||||
}
|
}
|
||||||
@@ -103,7 +190,10 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
for (final entry in _visibleFieldsBySensor.entries) {
|
for (final entry in _visibleFieldsBySensor.entries) {
|
||||||
encoded[entry.key] = entry.value.toList();
|
encoded[entry.key] = entry.value.toList();
|
||||||
}
|
}
|
||||||
await prefs.setString(_visibleSensorMetricsKey, jsonEncode(encoded));
|
await prefs.setString(
|
||||||
|
_key(_visibleSensorMetricsKey),
|
||||||
|
jsonEncode(encoded),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error saving visible sensor metrics: $e');
|
debugPrint('Error saving visible sensor metrics: $e');
|
||||||
}
|
}
|
||||||
@@ -112,16 +202,71 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
Future<void> _persistFieldSpans() async {
|
Future<void> _persistFieldSpans() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setString(_fieldSpanKey, jsonEncode(_fieldSpansBySensor));
|
await prefs.setString(
|
||||||
|
_key(_fieldSpanKey),
|
||||||
|
jsonEncode(_fieldSpansBySensor),
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('Error saving sensor field spans: $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(
|
Set<String> visibleFieldsFor(String publicKeyHex) => Set<String>.unmodifiable(
|
||||||
_visibleFieldsBySensor[publicKeyHex] ?? _defaultVisibleFields,
|
_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) =>
|
bool showsField(String publicKeyHex, String fieldKey) =>
|
||||||
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
visibleFieldsFor(publicKeyHex).contains(fieldKey);
|
||||||
|
|
||||||
@@ -131,6 +276,118 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
return span == 2 ? 2 : 1;
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isRefreshDue(
|
||||||
|
String publicKeyHex, {
|
||||||
|
required int minutes,
|
||||||
|
required DateTime refreshTime,
|
||||||
|
}) {
|
||||||
|
if (minutes <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
||||||
|
return lastRefreshAt == null ||
|
||||||
|
refreshTime.difference(lastRefreshAt) >= Duration(minutes: minutes);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> toggleMetric(
|
Future<void> toggleMetric(
|
||||||
String publicKeyHex,
|
String publicKeyHex,
|
||||||
String fieldKey,
|
String fieldKey,
|
||||||
@@ -140,8 +397,17 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
publicKeyHex,
|
publicKeyHex,
|
||||||
() => Set<String>.from(_defaultVisibleFields),
|
() => Set<String>.from(_defaultVisibleFields),
|
||||||
);
|
);
|
||||||
|
final metricOrder = _metricOrderBySensor.putIfAbsent(
|
||||||
|
publicKeyHex,
|
||||||
|
() => List<String>.from(_defaultMetricOrder),
|
||||||
|
);
|
||||||
|
var shouldPersistOrder = false;
|
||||||
if (visible) {
|
if (visible) {
|
||||||
visibleFields.add(fieldKey);
|
visibleFields.add(fieldKey);
|
||||||
|
if (!metricOrder.contains(fieldKey)) {
|
||||||
|
metricOrder.add(fieldKey);
|
||||||
|
shouldPersistOrder = true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (visibleFields.length == 1 && visibleFields.contains(fieldKey)) {
|
if (visibleFields.length == 1 && visibleFields.contains(fieldKey)) {
|
||||||
return;
|
return;
|
||||||
@@ -149,6 +415,9 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
visibleFields.remove(fieldKey);
|
visibleFields.remove(fieldKey);
|
||||||
}
|
}
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
|
if (shouldPersistOrder) {
|
||||||
|
await _persistMetricOrder();
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +435,84 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
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) =>
|
bool isWatched(String publicKeyHex) =>
|
||||||
_watchedSensorKeys.contains(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),
|
||||||
|
);
|
||||||
|
final selfTelemetry = contactsProvider.selfTelemetry;
|
||||||
|
if (existing != null) {
|
||||||
|
return selfTelemetry == null
|
||||||
|
? existing
|
||||||
|
: existing.copyWith(telemetry: selfTelemetry);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildSelfCandidate(connectionProvider, telemetry: selfTelemetry);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> addSensor(Contact contact) async {
|
Future<void> addSensor(Contact contact) async {
|
||||||
if (!contact.isChat && !contact.isRepeater) {
|
if (!contact.isChat && !contact.isRepeater && !contact.isSensor) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (_watchedSensorKeys.contains(contact.publicKeyHex)) {
|
if (_watchedSensorKeys.contains(contact.publicKeyHex)) {
|
||||||
@@ -178,43 +520,183 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_watchedSensorKeys.add(contact.publicKeyHex);
|
_watchedSensorKeys.add(contact.publicKeyHex);
|
||||||
|
final initialVisibleFields = _initialVisibleFieldsForContact(contact);
|
||||||
await _persistWatchedSensors();
|
await _persistWatchedSensors();
|
||||||
_visibleFieldsBySensor[contact.publicKeyHex] = Set<String>.from(
|
_visibleFieldsBySensor[contact.publicKeyHex] = initialVisibleFields;
|
||||||
_defaultVisibleFields,
|
|
||||||
);
|
|
||||||
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
_fieldSpansBySensor[contact.publicKeyHex] = <String, int>{'gps': 2};
|
||||||
|
_metricLabelsBySensor[contact.publicKeyHex] = <String, String>{};
|
||||||
|
_metricOrderBySensor[contact.publicKeyHex] = metricOrderFor(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
initialVisibleFields,
|
||||||
|
);
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
await _persistFieldSpans();
|
await _persistFieldSpans();
|
||||||
|
await _persistMetricLabels();
|
||||||
|
await _persistMetricOrder();
|
||||||
notifyListeners();
|
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 {
|
Future<void> removeSensor(String publicKeyHex) async {
|
||||||
_watchedSensorKeys.remove(publicKeyHex);
|
_watchedSensorKeys.remove(publicKeyHex);
|
||||||
_refreshStates.remove(publicKeyHex);
|
_refreshStates.remove(publicKeyHex);
|
||||||
_refreshStateUpdatedAt.remove(publicKeyHex);
|
_refreshStateUpdatedAt.remove(publicKeyHex);
|
||||||
_visibleFieldsBySensor.remove(publicKeyHex);
|
_visibleFieldsBySensor.remove(publicKeyHex);
|
||||||
_fieldSpansBySensor.remove(publicKeyHex);
|
_fieldSpansBySensor.remove(publicKeyHex);
|
||||||
|
_metricLabelsBySensor.remove(publicKeyHex);
|
||||||
|
_metricOrderBySensor.remove(publicKeyHex);
|
||||||
|
_autoRefreshMinutesBySensor.remove(publicKeyHex);
|
||||||
|
_lastRefreshAttemptAt.remove(publicKeyHex);
|
||||||
await _persistWatchedSensors();
|
await _persistWatchedSensors();
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
await _persistFieldSpans();
|
await _persistFieldSpans();
|
||||||
|
await _persistMetricLabels();
|
||||||
|
await _persistMetricOrder();
|
||||||
|
await _persistAutoRefreshMinutes();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
List<Contact> availableCandidates(ContactsProvider contactsProvider) {
|
Future<void> reorderSensors(int oldIndex, int newIndex) async {
|
||||||
|
if (_watchedSensorKeys.length < 2) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (oldIndex < 0 ||
|
||||||
|
oldIndex >= _watchedSensorKeys.length ||
|
||||||
|
newIndex < 0 ||
|
||||||
|
newIndex > _watchedSensorKeys.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetIndex = newIndex;
|
||||||
|
if (oldIndex < targetIndex) {
|
||||||
|
targetIndex -= 1;
|
||||||
|
}
|
||||||
|
if (oldIndex == targetIndex) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final movedKey = _watchedSensorKeys.removeAt(oldIndex);
|
||||||
|
_watchedSensorKeys.insert(targetIndex, movedKey);
|
||||||
|
await _persistWatchedSensors();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Contact> availableCandidates(
|
||||||
|
ContactsProvider contactsProvider, {
|
||||||
|
ConnectionProvider? connectionProvider,
|
||||||
|
}) {
|
||||||
final candidates = <Contact>[
|
final candidates = <Contact>[
|
||||||
...contactsProvider.chatContacts,
|
...contactsProvider.chatContacts,
|
||||||
...contactsProvider.repeaters,
|
...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.removeWhere((contact) => isWatched(contact.publicKeyHex));
|
||||||
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
candidates.sort((a, b) => b.lastSeenTime.compareTo(a.lastSeenTime));
|
||||||
return candidates;
|
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];
|
||||||
|
}
|
||||||
|
|
||||||
|
String? displaySelfKey({
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) {
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self == null || _watchedSensorKeys.contains(self.publicKeyHex)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return 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({
|
Future<void> refreshAll({
|
||||||
required ContactsProvider contactsProvider,
|
required ContactsProvider contactsProvider,
|
||||||
required ConnectionProvider connectionProvider,
|
required ConnectionProvider connectionProvider,
|
||||||
}) async {
|
}) async {
|
||||||
if (_isRefreshingAll || _watchedSensorKeys.isEmpty) {
|
if (_isRefreshingAll) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
final keysToRefresh = <String>[
|
||||||
|
if (self != null) self.publicKeyHex,
|
||||||
|
..._watchedSensorKeys.where(
|
||||||
|
(key) => self == null || key != self.publicKeyHex,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
if (keysToRefresh.isEmpty) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,7 +704,7 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for (final key in _watchedSensorKeys) {
|
for (final key in keysToRefresh) {
|
||||||
await refreshSensor(
|
await refreshSensor(
|
||||||
publicKeyHex: key,
|
publicKeyHex: key,
|
||||||
contactsProvider: contactsProvider,
|
contactsProvider: contactsProvider,
|
||||||
@@ -239,15 +721,20 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
required String publicKeyHex,
|
required String publicKeyHex,
|
||||||
required ContactsProvider contactsProvider,
|
required ContactsProvider contactsProvider,
|
||||||
required ConnectionProvider connectionProvider,
|
required ConnectionProvider connectionProvider,
|
||||||
|
DateTime? requestedAt,
|
||||||
}) async {
|
}) async {
|
||||||
Contact? contact;
|
if (stateFor(publicKeyHex) == SensorRefreshState.refreshing) {
|
||||||
for (final entry in contactsProvider.contacts) {
|
return;
|
||||||
if (entry.publicKeyHex == publicKeyHex) {
|
|
||||||
contact = entry;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_lastRefreshAttemptAt[publicKeyHex] = requestedAt ?? DateTime.now();
|
||||||
|
|
||||||
|
final contact = contactForDisplay(
|
||||||
|
publicKeyHex,
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
|
||||||
if (contact == null) {
|
if (contact == null) {
|
||||||
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
_setRefreshState(publicKeyHex, SensorRefreshState.unavailable);
|
||||||
return;
|
return;
|
||||||
@@ -266,6 +753,48 @@ 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).toList();
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self != null &&
|
||||||
|
!dueKeys.contains(self.publicKeyHex) &&
|
||||||
|
_isRefreshDue(
|
||||||
|
self.publicKeyHex,
|
||||||
|
minutes: selfAutoRefreshMinutes,
|
||||||
|
refreshTime: refreshTime,
|
||||||
|
)) {
|
||||||
|
dueKeys.insert(0, self.publicKeyHex);
|
||||||
|
}
|
||||||
|
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}) {
|
void clearExpiredRefreshStates({DateTime? now}) {
|
||||||
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
|
final cutoff = (now ?? DateTime.now()).subtract(_successStateRetention);
|
||||||
final keysToClear = <String>[];
|
final keysToClear = <String>[];
|
||||||
@@ -297,4 +826,29 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
_refreshStateUpdatedAt[publicKeyHex] = DateTime.now();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Contact? _buildSelfCandidate(
|
||||||
|
ConnectionProvider connectionProvider, {
|
||||||
|
ContactTelemetry? telemetry,
|
||||||
|
}) {
|
||||||
|
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,
|
||||||
|
telemetry: telemetry,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:typed_data';
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
|
import '../services/profiles_feature_service.dart';
|
||||||
import 'helpers/raw_session_retransmit.dart';
|
import 'helpers/raw_session_retransmit.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
import '../services/voice_codec_service.dart';
|
import '../services/voice_codec_service.dart';
|
||||||
@@ -92,6 +93,9 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
_restorePersistedVoiceData();
|
_restorePersistedVoiceData();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _storageKey() =>
|
||||||
|
ProfileStorageScope.scopedKey(_voiceSessionsStorageKey);
|
||||||
|
|
||||||
// ── Session accessors ────────────────────────────────────────────────────
|
// ── Session accessors ────────────────────────────────────────────────────
|
||||||
|
|
||||||
VoiceSession? session(String sessionId) => _sessions[sessionId];
|
VoiceSession? session(String sessionId) => _sessions[sessionId];
|
||||||
@@ -329,19 +333,22 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearStoredVoiceData() async {
|
Future<void> clearStoredVoiceData() async {
|
||||||
_sessions.clear();
|
await _resetInMemoryState();
|
||||||
_outgoingSessions.clear();
|
|
||||||
_ignoredIncomingSessions.clear();
|
|
||||||
_playingSessionId = null;
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_voiceSessionsStorageKey);
|
await prefs.remove(_storageKey());
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [VoiceProvider] Failed to clear stored voice data: $e');
|
debugPrint('❌ [VoiceProvider] Failed to clear stored voice data: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> reloadProfileScopedState() async {
|
||||||
|
await _resetInMemoryState();
|
||||||
|
await _restorePersistedVoiceData();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _persistVoiceData() async {
|
Future<void> _persistVoiceData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -365,7 +372,7 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
)
|
)
|
||||||
.toList(),
|
.toList(),
|
||||||
};
|
};
|
||||||
await prefs.setString(_voiceSessionsStorageKey, jsonEncode(payload));
|
await prefs.setString(_storageKey(), jsonEncode(payload));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [VoiceProvider] Failed to persist voice data: $e');
|
debugPrint('❌ [VoiceProvider] Failed to persist voice data: $e');
|
||||||
}
|
}
|
||||||
@@ -374,7 +381,7 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
Future<void> _restorePersistedVoiceData() async {
|
Future<void> _restorePersistedVoiceData() async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final raw = prefs.getString(_voiceSessionsStorageKey);
|
final raw = prefs.getString(_storageKey());
|
||||||
if (raw == null || raw.isEmpty) return;
|
if (raw == null || raw.isEmpty) return;
|
||||||
|
|
||||||
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
final parsed = jsonDecode(raw) as Map<String, dynamic>;
|
||||||
@@ -437,6 +444,16 @@ class VoiceProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _resetInMemoryState() async {
|
||||||
|
if (_playingSessionId != null || _player.isPlaying) {
|
||||||
|
await _player.stop();
|
||||||
|
}
|
||||||
|
_sessions.clear();
|
||||||
|
_outgoingSessions.clear();
|
||||||
|
_ignoredIncomingSessions.clear();
|
||||||
|
_playingSessionId = null;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_playerEventsSub.cancel();
|
_playerEventsSub.cancel();
|
||||||
|
|||||||
394
lib/screens/add_contact_screen.dart
Normal file
394
lib/screens/add_contact_screen.dart
Normal file
@@ -0,0 +1,394 @@
|
|||||||
|
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 {
|
||||||
|
final String? initialAdvert;
|
||||||
|
|
||||||
|
const AddContactScreen({super.key, this.initialAdvert});
|
||||||
|
|
||||||
|
@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();
|
||||||
|
if (widget.initialAdvert != null &&
|
||||||
|
widget.initialAdvert!.trim().isNotEmpty) {
|
||||||
|
_advertController.text = widget.initialAdvert!.trim();
|
||||||
|
} else {
|
||||||
|
_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
1028
lib/screens/discovery_screen.dart
Normal file
1028
lib/screens/discovery_screen.dart
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -6,12 +8,13 @@ import 'package:shared_preferences/shared_preferences.dart';
|
|||||||
import 'package:vibration/vibration.dart';
|
import 'package:vibration/vibration.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../providers/app_provider.dart';
|
import '../providers/app_provider.dart';
|
||||||
import '../models/device_info.dart' show ConnectionMode;
|
import '../models/device_info.dart' show ConnectionMode, DeviceInfo;
|
||||||
import '../providers/messages_provider.dart';
|
import '../providers/messages_provider.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../theme/app_theme.dart';
|
import '../theme/app_theme.dart';
|
||||||
import 'messages_tab.dart';
|
import 'messages_tab.dart';
|
||||||
import 'contacts_tab.dart';
|
import 'contacts_tab.dart';
|
||||||
|
import 'discovery_screen.dart';
|
||||||
import 'sensors_tab.dart';
|
import 'sensors_tab.dart';
|
||||||
import 'map_tab.dart';
|
import 'map_tab.dart';
|
||||||
import 'repeaters_map_screen.dart';
|
import 'repeaters_map_screen.dart';
|
||||||
@@ -19,6 +22,7 @@ import 'settings_screen.dart';
|
|||||||
import 'device_config_screen.dart';
|
import 'device_config_screen.dart';
|
||||||
import 'packet_log_screen.dart';
|
import 'packet_log_screen.dart';
|
||||||
import 'live_traffic_screen.dart';
|
import 'live_traffic_screen.dart';
|
||||||
|
import 'profiles_screen.dart';
|
||||||
import 'spectrum_scan_screen.dart';
|
import 'spectrum_scan_screen.dart';
|
||||||
import '../utils/toast_logger.dart';
|
import '../utils/toast_logger.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
@@ -27,6 +31,10 @@ import '../widgets/connection_dialog.dart';
|
|||||||
import '../utils/battery_display_helper.dart';
|
import '../utils/battery_display_helper.dart';
|
||||||
import '../services/developer_mode_service.dart';
|
import '../services/developer_mode_service.dart';
|
||||||
import '../services/mesh_map_nodes_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 }
|
enum _HomeTab { messages, contacts, sensors, map }
|
||||||
|
|
||||||
@@ -56,6 +64,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
with TickerProviderStateMixin, WidgetsBindingObserver {
|
with TickerProviderStateMixin, WidgetsBindingObserver {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
late final AppProvider _appProvider;
|
late final AppProvider _appProvider;
|
||||||
|
late final ConnectionProvider _connectionProvider;
|
||||||
int _currentIndex = 0;
|
int _currentIndex = 0;
|
||||||
bool _isMapFullscreen = false;
|
bool _isMapFullscreen = false;
|
||||||
bool _showRxTxIndicators = true;
|
bool _showRxTxIndicators = true;
|
||||||
@@ -64,6 +73,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
bool _isContactsEnabled = true;
|
bool _isContactsEnabled = true;
|
||||||
bool _isSensorsEnabled = false;
|
bool _isSensorsEnabled = false;
|
||||||
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
AppLifecycleState _lifecycleState = AppLifecycleState.resumed;
|
||||||
|
String? _lastProfileDeviceKey;
|
||||||
|
|
||||||
List<_HomeTab> get _enabledTabs {
|
List<_HomeTab> get _enabledTabs {
|
||||||
return [
|
return [
|
||||||
@@ -87,6 +97,8 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
super.initState();
|
super.initState();
|
||||||
WidgetsBinding.instance.addObserver(this);
|
WidgetsBinding.instance.addObserver(this);
|
||||||
_appProvider = context.read<AppProvider>();
|
_appProvider = context.read<AppProvider>();
|
||||||
|
_connectionProvider = context.read<ConnectionProvider>();
|
||||||
|
_connectionProvider.addListener(_handleConnectionProviderChanged);
|
||||||
_isMapEnabled = _appProvider.isMapEnabled;
|
_isMapEnabled = _appProvider.isMapEnabled;
|
||||||
_isContactsEnabled = _appProvider.isContactsEnabled;
|
_isContactsEnabled = _appProvider.isContactsEnabled;
|
||||||
_isSensorsEnabled = _appProvider.isSensorsEnabled;
|
_isSensorsEnabled = _appProvider.isSensorsEnabled;
|
||||||
@@ -104,6 +116,10 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
_showPermissionDialog();
|
_showPermissionDialog();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_handleConnectionProviderChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initTabController() {
|
void _initTabController() {
|
||||||
@@ -219,6 +235,36 @@ 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) {
|
void _openLiveTraffic(ConnectionProvider provider) {
|
||||||
openLiveTrafficScreen(context, provider);
|
openLiveTrafficScreen(context, provider);
|
||||||
}
|
}
|
||||||
@@ -236,7 +282,11 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_showRxTxIndicators = prefs.getBool('show_rx_tx_indicators') ?? true;
|
_showRxTxIndicators =
|
||||||
|
prefs.getBool(
|
||||||
|
ProfileStorageScope.scopedKey('show_rx_tx_indicators'),
|
||||||
|
) ??
|
||||||
|
true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,16 +299,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() {
|
void _showPermissionDialog() {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@@ -354,7 +394,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(l10n.flood),
|
title: Text(l10n.flood),
|
||||||
subtitle: const Text('Relay through repeaters across the mesh'),
|
subtitle: Text(l10n.relayThroughRepeatersAcrossTheMesh),
|
||||||
trailing: const Icon(Icons.chevron_right_rounded),
|
trailing: const Icon(Icons.chevron_right_rounded),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -380,7 +420,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
title: Text(l10n.direct),
|
title: Text(l10n.direct),
|
||||||
subtitle: const Text('Nearby only, without repeater flooding'),
|
subtitle: Text(l10n.nearbyOnlyWithoutRepeaterFlooding),
|
||||||
trailing: const Icon(Icons.chevron_right_rounded),
|
trailing: const Icon(Icons.chevron_right_rounded),
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
@@ -394,45 +434,184 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildActivityBadge({
|
void _showDeviceInfoSheet(BuildContext context, DeviceInfo deviceInfo) {
|
||||||
required String label,
|
// Request self telemetry so it's fresh
|
||||||
required int count,
|
context.read<ConnectionProvider>().requestSelfTelemetry();
|
||||||
required bool isActive,
|
|
||||||
required Color activeColor,
|
showModalBottomSheet(
|
||||||
bool compact = false,
|
context: context,
|
||||||
}) {
|
shape: const RoundedRectangleBorder(
|
||||||
final color = isActive ? activeColor : Colors.grey;
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
return Container(
|
|
||||||
padding: EdgeInsets.symmetric(
|
|
||||||
horizontal: compact ? 7 : 8,
|
|
||||||
vertical: compact ? 4 : 5,
|
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
builder: (context) => Consumer<ContactsProvider>(
|
||||||
color: color.withValues(alpha: 0.12),
|
builder: (context, contactsProvider, child) {
|
||||||
borderRadius: BorderRadius.circular(999),
|
final telemetry = contactsProvider.selfTelemetry;
|
||||||
),
|
|
||||||
child: Row(
|
return SafeArea(
|
||||||
mainAxisSize: MainAxisSize.min,
|
child: Padding(
|
||||||
children: [
|
padding: const EdgeInsets.all(20),
|
||||||
Container(
|
child: Column(
|
||||||
width: compact ? 6 : 7,
|
mainAxisSize: MainAxisSize.min,
|
||||||
height: compact ? 6 : 7,
|
children: [
|
||||||
decoration: BoxDecoration(shape: BoxShape.circle, color: color),
|
Container(
|
||||||
),
|
width: 40,
|
||||||
SizedBox(width: compact ? 5 : 6),
|
height: 4,
|
||||||
Text(
|
decoration: BoxDecoration(
|
||||||
'$label:$count',
|
color: Theme.of(context).dividerColor,
|
||||||
style: TextStyle(
|
borderRadius: BorderRadius.circular(2),
|
||||||
fontSize: compact ? 10 : 11,
|
),
|
||||||
fontWeight: FontWeight.w600,
|
),
|
||||||
color: color,
|
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),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
value,
|
||||||
|
style: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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({
|
Widget _buildCompactActivityIndicator({
|
||||||
required bool rxActive,
|
required bool rxActive,
|
||||||
required bool txActive,
|
required bool txActive,
|
||||||
@@ -589,6 +768,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
}
|
}
|
||||||
|
|
||||||
context.watch<AppProvider>();
|
context.watch<AppProvider>();
|
||||||
|
final activeProfileId = context.select<ProfileManager, String>(
|
||||||
|
(manager) => manager.activeProfileId,
|
||||||
|
);
|
||||||
|
|
||||||
final enabledTabs = _enabledTabs;
|
final enabledTabs = _enabledTabs;
|
||||||
final isMapTabActive = _currentTab == _HomeTab.map;
|
final isMapTabActive = _currentTab == _HomeTab.map;
|
||||||
@@ -613,7 +795,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await provider.disconnect();
|
await provider.disconnect();
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.power_settings_new),
|
icon: Icon(Icons.power_settings_new),
|
||||||
tooltip: AppLocalizations.of(context)!.disconnect,
|
tooltip: AppLocalizations.of(context)!.disconnect,
|
||||||
color: Colors.red.shade700,
|
color: Colors.red.shade700,
|
||||||
);
|
);
|
||||||
@@ -625,6 +807,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
icon: const Icon(Icons.more_vert),
|
icon: const Icon(Icons.more_vert),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
final items = <PopupMenuEntry<void>>[];
|
final items = <PopupMenuEntry<void>>[];
|
||||||
|
final profilesEnabled = context
|
||||||
|
.read<ProfileManager>()
|
||||||
|
.profilesEnabled;
|
||||||
|
|
||||||
if (_isDeveloperModeEnabled) {
|
if (_isDeveloperModeEnabled) {
|
||||||
items.add(
|
items.add(
|
||||||
@@ -632,8 +817,8 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.radar),
|
const Icon(Icons.radar),
|
||||||
const SizedBox(width: 8),
|
SizedBox(width: 8),
|
||||||
const Text('Spectrum Scan'),
|
Text(AppLocalizations.of(context)!.spectrumScan),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -654,11 +839,11 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: const Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.radar_outlined),
|
const Icon(Icons.radar_outlined),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Live Traffic'),
|
Text(AppLocalizations.of(context)!.liveTraffic),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -689,11 +874,11 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
|
|
||||||
items.add(
|
items.add(
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: const Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.router_outlined),
|
const Icon(Icons.router_outlined),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Repeaters Map'),
|
Text(AppLocalizations.of(context)!.repeatersMap),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
@@ -716,8 +901,41 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.settings),
|
const Icon(Icons.person_search),
|
||||||
const SizedBox(width: 8),
|
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),
|
Text(AppLocalizations.of(context)!.settings),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -742,6 +960,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;
|
return items;
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -768,9 +1011,14 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
onNavigateToMessages: () => _navigateToTab(_HomeTab.messages),
|
||||||
);
|
);
|
||||||
case _HomeTab.sensors:
|
case _HomeTab.sensors:
|
||||||
return const SensorsTab();
|
return SensorsTab(
|
||||||
|
isActive:
|
||||||
|
_currentTab == _HomeTab.sensors &&
|
||||||
|
_lifecycleState == AppLifecycleState.resumed,
|
||||||
|
);
|
||||||
case _HomeTab.map:
|
case _HomeTab.map:
|
||||||
return MapTab(
|
return MapTab(
|
||||||
|
key: ValueKey<String>('map:$activeProfileId'),
|
||||||
onFullscreenChanged: (isFullscreen) {
|
onFullscreenChanged: (isFullscreen) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_isMapFullscreen = isFullscreen;
|
_isMapFullscreen = isFullscreen;
|
||||||
@@ -827,7 +1075,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
);
|
);
|
||||||
case _HomeTab.map:
|
case _HomeTab.map:
|
||||||
return Tab(
|
return Tab(
|
||||||
icon: const Icon(Icons.map),
|
icon: Icon(Icons.map),
|
||||||
text: AppLocalizations.of(context)!.map,
|
text: AppLocalizations.of(context)!.map,
|
||||||
);
|
);
|
||||||
case _HomeTab.sensors:
|
case _HomeTab.sensors:
|
||||||
@@ -852,19 +1100,35 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
final isTcpConnected = provider.connectionMode == ConnectionMode.tcp;
|
||||||
|
|
||||||
if (!isConnected) {
|
if (!isConnected) {
|
||||||
// Disconnected state: show connect button
|
final buttonLabel = provider.isReconnecting
|
||||||
|
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
||||||
|
: AppLocalizations.of(context)!.connect;
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Column(
|
||||||
AppLocalizations.of(context)!.appTitle,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: const TextStyle(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 20,
|
children: [
|
||||||
fontWeight: FontWeight.bold,
|
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
|
onPressed: provider.isReconnecting
|
||||||
? null
|
? null
|
||||||
: () => _showConnectionDialog(context),
|
: () => _showConnectionDialog(context),
|
||||||
@@ -875,22 +1139,19 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(
|
||||||
strokeWidth: 2,
|
strokeWidth: 2,
|
||||||
valueColor: AlwaysStoppedAnimation<Color>(
|
valueColor: AlwaysStoppedAnimation<Color>(
|
||||||
Colors.black54,
|
Colors.white70,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: const Icon(Icons.bluetooth, size: 18),
|
: const Icon(Icons.add_link_rounded, size: 18),
|
||||||
label: Text(
|
label: Text(buttonLabel),
|
||||||
provider.isReconnecting
|
style: FilledButton.styleFrom(
|
||||||
? '${provider.reconnectionAttempt}/${provider.maxReconnectionAttempts}'
|
padding: const EdgeInsets.symmetric(
|
||||||
: AppLocalizations.of(context)!.connect,
|
horizontal: 16,
|
||||||
),
|
vertical: 12,
|
||||||
style: ElevatedButton.styleFrom(
|
),
|
||||||
backgroundColor: Colors.white,
|
|
||||||
foregroundColor: Colors.black87,
|
|
||||||
elevation: 0,
|
|
||||||
shape: RoundedRectangleBorder(
|
shape: RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(16),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -898,7 +1159,7 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => provider.cancelReconnection(),
|
onPressed: () => provider.cancelReconnection(),
|
||||||
icon: const Icon(Icons.close, size: 20),
|
icon: Icon(Icons.close, size: 20),
|
||||||
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
tooltip: AppLocalizations.of(context)!.cancelReconnection,
|
||||||
style: IconButton.styleFrom(
|
style: IconButton.styleFrom(
|
||||||
backgroundColor: Colors.red.shade700,
|
backgroundColor: Colors.red.shade700,
|
||||||
@@ -957,50 +1218,45 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(height: 2),
|
||||||
Row(
|
GestureDetector(
|
||||||
mainAxisSize: MainAxisSize.min,
|
onTap: () =>
|
||||||
children: [
|
_showDeviceInfoSheet(context, deviceInfo),
|
||||||
Icon(
|
child: Row(
|
||||||
isTcpConnected
|
mainAxisSize: MainAxisSize.min,
|
||||||
? Icons.wifi_rounded
|
children: [
|
||||||
: Icons.bluetooth_connected_rounded,
|
|
||||||
size: 13,
|
|
||||||
color: signalColor,
|
|
||||||
),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
if (deviceInfo.batteryPercent != null) ...[
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Icon(
|
Icon(
|
||||||
BatteryDisplayHelper.getBatteryIcon(
|
isTcpConnected
|
||||||
deviceInfo.batteryPercent!,
|
? Icons.wifi_rounded
|
||||||
),
|
: Icons.bluetooth_connected_rounded,
|
||||||
size: 13,
|
size: 13,
|
||||||
color:
|
color: signalColor,
|
||||||
BatteryDisplayHelper.getBatteryColor(
|
|
||||||
deviceInfo.batteryPercent!,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
if (!isTcpConnected &&
|
||||||
SizedBox(
|
deviceInfo.signalRssi != null) ...[
|
||||||
width: 30,
|
const SizedBox(width: 4),
|
||||||
child: Text(
|
_buildMiniSignalBars(
|
||||||
|
activeBars:
|
||||||
|
BatteryDisplayHelper.getSignalBars(
|
||||||
|
deviceInfo.signalRssi!,
|
||||||
|
),
|
||||||
|
color: signalColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
if (deviceInfo.batteryPercent != null) ...[
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
BatteryDisplayHelper.getBatteryIcon(
|
||||||
|
deviceInfo.batteryPercent!,
|
||||||
|
),
|
||||||
|
size: 13,
|
||||||
|
color:
|
||||||
|
BatteryDisplayHelper.getBatteryColor(
|
||||||
|
deviceInfo.batteryPercent!,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 2),
|
||||||
|
Text(
|
||||||
'${deviceInfo.batteryPercent!.round()}%',
|
'${deviceInfo.batteryPercent!.round()}%',
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
@@ -1010,9 +1266,9 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1109,48 +1365,13 @@ class _HomeScreenState extends State<HomeScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
child: isTight
|
child: _buildCompactActivityIndicator(
|
||||||
? _buildCompactActivityIndicator(
|
rxActive: provider.rxActivity,
|
||||||
rxActive: provider.rxActivity,
|
txActive: provider.txActivity,
|
||||||
txActive: provider.txActivity,
|
),
|
||||||
)
|
|
||||||
: Container(
|
|
||||||
constraints: const BoxConstraints(minHeight: 48),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 8,
|
|
||||||
vertical: 4,
|
|
||||||
),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.surfaceContainerHigh
|
|
||||||
.withValues(alpha: 0.85),
|
|
||||||
borderRadius: BorderRadius.circular(22),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
_buildActivityBadge(
|
|
||||||
label: 'RX',
|
|
||||||
count: provider.rxPacketCount,
|
|
||||||
isActive: provider.rxActivity,
|
|
||||||
activeColor: Colors.green,
|
|
||||||
compact: true,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
_buildActivityBadge(
|
|
||||||
label: 'TX',
|
|
||||||
count: provider.txPacketCount,
|
|
||||||
isActive: provider.txActivity,
|
|
||||||
activeColor: Colors.blue,
|
|
||||||
compact: true,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
SizedBox(width: isTight ? 24 : 74),
|
const SizedBox(width: 24),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'package:crypto/crypto.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -11,7 +12,10 @@ import '../services/live_traffic_summary.dart';
|
|||||||
import '../services/location_tracking_service.dart';
|
import '../services/location_tracking_service.dart';
|
||||||
import '../services/route_hash_preferences.dart';
|
import '../services/route_hash_preferences.dart';
|
||||||
import '../utils/log_rx_route_decoder.dart';
|
import '../utils/log_rx_route_decoder.dart';
|
||||||
|
import '../widgets/compact_signal_indicator.dart';
|
||||||
|
import '../widgets/messages/message_trace_sheet.dart';
|
||||||
import 'packet_log_screen.dart';
|
import 'packet_log_screen.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
T? _maybeProvider<T>(BuildContext context) {
|
T? _maybeProvider<T>(BuildContext context) {
|
||||||
try {
|
try {
|
||||||
@@ -23,7 +27,6 @@ T? _maybeProvider<T>(BuildContext context) {
|
|||||||
|
|
||||||
class LiveTrafficScreen extends StatefulWidget {
|
class LiveTrafficScreen extends StatefulWidget {
|
||||||
final List<BlePacketLog> Function() logReader;
|
final List<BlePacketLog> Function() logReader;
|
||||||
final int Function()? rxCountReader;
|
|
||||||
final Listenable? refreshListenable;
|
final Listenable? refreshListenable;
|
||||||
final DateTime Function() now;
|
final DateTime Function() now;
|
||||||
final VoidCallback? openPacketLogs;
|
final VoidCallback? openPacketLogs;
|
||||||
@@ -31,7 +34,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
|||||||
const LiveTrafficScreen({
|
const LiveTrafficScreen({
|
||||||
super.key,
|
super.key,
|
||||||
required this.logReader,
|
required this.logReader,
|
||||||
this.rxCountReader,
|
|
||||||
this.refreshListenable,
|
this.refreshListenable,
|
||||||
DateTime Function()? now,
|
DateTime Function()? now,
|
||||||
this.openPacketLogs,
|
this.openPacketLogs,
|
||||||
@@ -45,7 +47,6 @@ class LiveTrafficScreen extends StatefulWidget {
|
|||||||
return LiveTrafficScreen(
|
return LiveTrafficScreen(
|
||||||
key: key,
|
key: key,
|
||||||
logReader: () => provider.bleService.packetLogs,
|
logReader: () => provider.bleService.packetLogs,
|
||||||
rxCountReader: () => provider.rxPacketCount,
|
|
||||||
refreshListenable: provider,
|
refreshListenable: provider,
|
||||||
openPacketLogs: openPacketLogs,
|
openPacketLogs: openPacketLogs,
|
||||||
);
|
);
|
||||||
@@ -114,15 +115,17 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final logs = widget.logReader();
|
||||||
|
final totalRxDataCount = LiveTrafficSummary.countRxDataLogs(logs);
|
||||||
final unfilteredSnapshot = LiveTrafficSummary.fromLogs(
|
final unfilteredSnapshot = LiveTrafficSummary.fromLogs(
|
||||||
widget.logReader(),
|
logs,
|
||||||
now: widget.now(),
|
now: widget.now(),
|
||||||
clearedAt: _clearedAt,
|
clearedAt: _clearedAt,
|
||||||
preferredHashSize: _preferredHashSize,
|
preferredHashSize: _preferredHashSize,
|
||||||
window: _selectedWindow,
|
window: _selectedWindow,
|
||||||
);
|
);
|
||||||
final snapshot = LiveTrafficSummary.fromLogs(
|
final snapshot = LiveTrafficSummary.fromLogs(
|
||||||
widget.logReader(),
|
logs,
|
||||||
now: widget.now(),
|
now: widget.now(),
|
||||||
clearedAt: _clearedAt,
|
clearedAt: _clearedAt,
|
||||||
preferredHashSize: _preferredHashSize,
|
preferredHashSize: _preferredHashSize,
|
||||||
@@ -134,16 +137,17 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
|||||||
final routeHashCounts = _RouteHashCounts.fromContacts(
|
final routeHashCounts = _RouteHashCounts.fromContacts(
|
||||||
contactsProvider?.contacts ?? const <Contact>[],
|
contactsProvider?.contacts ?? const <Contact>[],
|
||||||
);
|
);
|
||||||
final packetTypes = unfilteredSnapshot.visibleEntries
|
final packetTypes =
|
||||||
.map((entry) => entry.payloadLabel)
|
unfilteredSnapshot.visibleEntries
|
||||||
.toSet()
|
.map((entry) => entry.payloadLabel)
|
||||||
.toList()
|
.toSet()
|
||||||
..sort();
|
.toList()
|
||||||
|
..sort();
|
||||||
final filteredEntries = snapshot.visibleEntries;
|
final filteredEntries = snapshot.visibleEntries;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Live Traffic'),
|
title: Text(AppLocalizations.of(context)!.liveTraffic),
|
||||||
actions: [
|
actions: [
|
||||||
if (widget.openPacketLogs != null)
|
if (widget.openPacketLogs != null)
|
||||||
IconButton(
|
IconButton(
|
||||||
@@ -172,7 +176,7 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
|||||||
children: [
|
children: [
|
||||||
_SummaryPanel(
|
_SummaryPanel(
|
||||||
snapshot: snapshot,
|
snapshot: snapshot,
|
||||||
totalRxCount: widget.rxCountReader?.call(),
|
totalRxCount: totalRxDataCount,
|
||||||
routeHashCounts: routeHashCounts,
|
routeHashCounts: routeHashCounts,
|
||||||
onWindowTap: () => _showWindowPicker(context),
|
onWindowTap: () => _showWindowPicker(context),
|
||||||
),
|
),
|
||||||
@@ -202,7 +206,9 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
|||||||
Icon(
|
Icon(
|
||||||
Icons.radar_rounded,
|
Icons.radar_rounded,
|
||||||
size: 64,
|
size: 64,
|
||||||
color: theme.colorScheme.primary.withValues(alpha: 0.45),
|
color: theme.colorScheme.primary.withValues(
|
||||||
|
alpha: 0.45,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
const Text(
|
const Text(
|
||||||
@@ -218,7 +224,9 @@ class _LiveTrafficScreenState extends State<LiveTrafficScreen> {
|
|||||||
? 'This view only shows in-memory traffic while it is active.'
|
? 'This view only shows in-memory traffic while it is active.'
|
||||||
: 'Try a different packet type or switch back to All.',
|
: 'Try a different packet type or switch back to All.',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: TextStyle(color: theme.colorScheme.onSurfaceVariant),
|
style: TextStyle(
|
||||||
|
color: theme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -297,8 +305,48 @@ class _SummaryPanel extends StatelessWidget {
|
|||||||
LiveTrafficBusyness.active => 'Active',
|
LiveTrafficBusyness.active => 'Active',
|
||||||
LiveTrafficBusyness.busy => 'Busy',
|
LiveTrafficBusyness.busy => 'Busy',
|
||||||
};
|
};
|
||||||
|
final metrics = [
|
||||||
|
_MetricTile(
|
||||||
|
tileKey: const ValueKey('liveTrafficMetric:rxPackets'),
|
||||||
|
label: AppLocalizations.of(context)!.rxPackets,
|
||||||
|
value: '${snapshot.rxCount}',
|
||||||
|
subtitle: totalRxCount == null
|
||||||
|
? _windowSummaryLabel(snapshot.windowDuration)
|
||||||
|
: 'Device total $totalRxCount',
|
||||||
|
),
|
||||||
|
_MetricTile(
|
||||||
|
tileKey: const ValueKey('liveTrafficMetric:rssi'),
|
||||||
|
label: AppLocalizations.of(context)!.rssi,
|
||||||
|
value: snapshot.latestRssiDbm == null
|
||||||
|
? 'No RX data'
|
||||||
|
: '${snapshot.latestRssiDbm} dBm',
|
||||||
|
subtitle: snapshot.avgRssiDbm == null
|
||||||
|
? 'No average yet'
|
||||||
|
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
||||||
|
),
|
||||||
|
_MetricTile(
|
||||||
|
tileKey: const ValueKey('liveTrafficMetric:snr'),
|
||||||
|
label: AppLocalizations.of(context)!.snr,
|
||||||
|
value: snapshot.latestSnrDb == null
|
||||||
|
? 'No RX data'
|
||||||
|
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
||||||
|
subtitle: snapshot.avgSnrDb == null
|
||||||
|
? 'No average yet'
|
||||||
|
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
||||||
|
),
|
||||||
|
_MetricTile(
|
||||||
|
tileKey: const ValueKey('liveTrafficMetric:multihop'),
|
||||||
|
label: AppLocalizations.of(context)!.multihop,
|
||||||
|
value: '${snapshot.multiHopCount}',
|
||||||
|
subtitle: routeHashCounts.summaryLabel,
|
||||||
|
footer: snapshot.avgHopCount == null
|
||||||
|
? 'No routes yet'
|
||||||
|
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
@@ -319,17 +367,17 @@ class _SummaryPanel extends StatelessWidget {
|
|||||||
runSpacing: 6,
|
runSpacing: 6,
|
||||||
children: [
|
children: [
|
||||||
_SummaryBadge(
|
_SummaryBadge(
|
||||||
label: 'Mesh',
|
label: AppLocalizations.of(context)!.mesh,
|
||||||
value: busynessLabel,
|
value: busynessLabel,
|
||||||
color: busynessColor,
|
color: busynessColor,
|
||||||
),
|
),
|
||||||
_SummaryBadge(
|
_SummaryBadge(
|
||||||
label: 'Rate',
|
label: AppLocalizations.of(context)!.rate,
|
||||||
value: '${snapshot.packetsPerMinute} pkt/min',
|
value: '${snapshot.packetsPerMinute} pkt/min',
|
||||||
color: Theme.of(context).colorScheme.primary,
|
color: Theme.of(context).colorScheme.primary,
|
||||||
),
|
),
|
||||||
_SummaryBadge(
|
_SummaryBadge(
|
||||||
label: 'Window',
|
label: AppLocalizations.of(context)!.window,
|
||||||
value: _windowLabel(snapshot.windowDuration),
|
value: _windowLabel(snapshot.windowDuration),
|
||||||
color: Theme.of(context).colorScheme.secondary,
|
color: Theme.of(context).colorScheme.secondary,
|
||||||
onTap: onWindowTap,
|
onTap: onWindowTap,
|
||||||
@@ -337,44 +385,26 @@ class _SummaryPanel extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
Wrap(
|
LayoutBuilder(
|
||||||
spacing: 10,
|
builder: (context, constraints) {
|
||||||
runSpacing: 10,
|
const spacing = 10.0;
|
||||||
children: [
|
const minTileWidth = 150.0;
|
||||||
_MetricTile(
|
final columns =
|
||||||
label: 'RX packets',
|
((constraints.maxWidth + spacing) / (minTileWidth + spacing))
|
||||||
value: '${snapshot.rxCount}',
|
.floor()
|
||||||
subtitle: totalRxCount == null
|
.clamp(1, metrics.length);
|
||||||
? _windowSummaryLabel(snapshot.windowDuration)
|
final tileWidth =
|
||||||
: 'Device total $totalRxCount',
|
(constraints.maxWidth - (spacing * (columns - 1))) / columns;
|
||||||
),
|
|
||||||
_MetricTile(
|
return Wrap(
|
||||||
label: 'RSSI',
|
spacing: spacing,
|
||||||
value: snapshot.latestRssiDbm == null
|
runSpacing: spacing,
|
||||||
? 'No RX data'
|
children: [
|
||||||
: '${snapshot.latestRssiDbm} dBm',
|
for (final metric in metrics)
|
||||||
subtitle: snapshot.avgRssiDbm == null
|
SizedBox(width: tileWidth, child: metric),
|
||||||
? 'No average yet'
|
],
|
||||||
: 'Avg ${snapshot.avgRssiDbm!.toStringAsFixed(1)} dBm',
|
);
|
||||||
),
|
},
|
||||||
_MetricTile(
|
|
||||||
label: 'SNR',
|
|
||||||
value: snapshot.latestSnrDb == null
|
|
||||||
? 'No RX data'
|
|
||||||
: '${snapshot.latestSnrDb!.toStringAsFixed(1)} dB',
|
|
||||||
subtitle: snapshot.avgSnrDb == null
|
|
||||||
? 'No average yet'
|
|
||||||
: 'Avg ${snapshot.avgSnrDb!.toStringAsFixed(1)} dB',
|
|
||||||
),
|
|
||||||
_MetricTile(
|
|
||||||
label: 'Multi-hop',
|
|
||||||
value: '${snapshot.multiHopCount}',
|
|
||||||
subtitle: routeHashCounts.summaryLabel,
|
|
||||||
footer: snapshot.avgHopCount == null
|
|
||||||
? 'No routes yet'
|
|
||||||
: 'Avg ${snapshot.avgHopCount!.toStringAsFixed(1)} hops',
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -447,7 +477,7 @@ class _PacketTypeFilterBar extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
_FilterChip(
|
_FilterChip(
|
||||||
label: 'All',
|
label: AppLocalizations.of(context)!.all,
|
||||||
selected: selectedType == null,
|
selected: selectedType == null,
|
||||||
onTap: () => onSelected(null),
|
onTap: () => onSelected(null),
|
||||||
),
|
),
|
||||||
@@ -488,7 +518,9 @@ class _FilterChip extends StatelessWidget {
|
|||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: color.withValues(alpha: selected ? 0.14 : 0.08),
|
color: color.withValues(alpha: selected ? 0.14 : 0.08),
|
||||||
borderRadius: BorderRadius.circular(999),
|
borderRadius: BorderRadius.circular(999),
|
||||||
border: Border.all(color: color.withValues(alpha: selected ? 0.45 : 0.22)),
|
border: Border.all(
|
||||||
|
color: color.withValues(alpha: selected ? 0.45 : 0.22),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
label,
|
label,
|
||||||
@@ -514,12 +546,14 @@ String _windowSummaryLabel(Duration duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MetricTile extends StatelessWidget {
|
class _MetricTile extends StatelessWidget {
|
||||||
|
final Key? tileKey;
|
||||||
final String label;
|
final String label;
|
||||||
final String value;
|
final String value;
|
||||||
final String subtitle;
|
final String subtitle;
|
||||||
final String? footer;
|
final String? footer;
|
||||||
|
|
||||||
const _MetricTile({
|
const _MetricTile({
|
||||||
|
this.tileKey,
|
||||||
required this.label,
|
required this.label,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.subtitle,
|
required this.subtitle,
|
||||||
@@ -529,7 +563,7 @@ class _MetricTile extends StatelessWidget {
|
|||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
width: 160,
|
key: tileKey,
|
||||||
height: 136,
|
height: 136,
|
||||||
padding: const EdgeInsets.all(12),
|
padding: const EdgeInsets.all(12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -641,103 +675,103 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
final isRx = log.direction == PacketDirection.rx;
|
final isRx = log.direction == PacketDirection.rx;
|
||||||
final accent = isRx ? Colors.green : Colors.blue;
|
final accent = isRx ? Colors.green : Colors.blue;
|
||||||
final rxInfo = log.logRxDataInfo;
|
final rxInfo = log.logRxDataInfo;
|
||||||
final routePreview = _resolvedRoutePreview(context, entry);
|
|
||||||
final originDistance = _originDistanceLabel(context, entry);
|
final originDistance = _originDistanceLabel(context, entry);
|
||||||
|
final packetDetails = _LiveTrafficPacketDetails.fromEntry(entry);
|
||||||
|
final signalMetric = SignalMetric.fromRxInfo(rxInfo);
|
||||||
|
|
||||||
return Container(
|
return Material(
|
||||||
padding: const EdgeInsets.all(14),
|
color: Colors.transparent,
|
||||||
decoration: BoxDecoration(
|
child: InkWell(
|
||||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
onTap: () => _showPacketBytesSheet(context, log.rawData),
|
||||||
|
onLongPress: () => _showTraceSheet(context, entry),
|
||||||
borderRadius: BorderRadius.circular(18),
|
borderRadius: BorderRadius.circular(18),
|
||||||
border: Border.all(color: accent.withValues(alpha: 0.25)),
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.all(14),
|
||||||
child: Column(
|
decoration: BoxDecoration(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||||
children: [
|
borderRadius: BorderRadius.circular(18),
|
||||||
Row(
|
border: Border.all(color: accent.withValues(alpha: 0.25)),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Row(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Container(
|
||||||
color: accent.withValues(alpha: 0.14),
|
padding: const EdgeInsets.symmetric(
|
||||||
borderRadius: BorderRadius.circular(999),
|
horizontal: 10,
|
||||||
),
|
vertical: 6,
|
||||||
child: Text(
|
),
|
||||||
isRx ? 'RX' : 'TX',
|
decoration: BoxDecoration(
|
||||||
style: TextStyle(
|
color: accent.withValues(alpha: 0.14),
|
||||||
color: accent,
|
borderRadius: BorderRadius.circular(999),
|
||||||
fontWeight: FontWeight.w800,
|
),
|
||||||
fontSize: 12,
|
child: Text(
|
||||||
),
|
isRx ? 'RX' : 'TX',
|
||||||
),
|
style: TextStyle(
|
||||||
),
|
color: accent,
|
||||||
const SizedBox(width: 8),
|
fontWeight: FontWeight.w800,
|
||||||
Expanded(
|
fontSize: 12,
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
entry.payloadLabel,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (entry.payloadMeaning != null)
|
),
|
||||||
Text(
|
const SizedBox(width: 8),
|
||||||
entry.payloadMeaning!,
|
Expanded(
|
||||||
maxLines: 1,
|
child: Column(
|
||||||
overflow: TextOverflow.ellipsis,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: TextStyle(
|
children: [
|
||||||
fontSize: 11,
|
Text(
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
packetDetails.title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
if (entry.payloadMeaning != null)
|
||||||
|
Text(
|
||||||
|
entry.payloadMeaning!,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (signalMetric != null) ...[
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
CompactSignalIndicator(metric: signalMetric),
|
||||||
|
] else
|
||||||
|
Text(
|
||||||
|
_timeAgo(log.timestamp, now),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
],
|
||||||
),
|
),
|
||||||
Text(
|
const SizedBox(height: 10),
|
||||||
_timeAgo(log.timestamp, now),
|
_PacketInfoLine(
|
||||||
style: TextStyle(
|
text:
|
||||||
fontSize: 12,
|
'${_formatClock(log.timestamp)} • Size: ${log.rawData.length} bytes',
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
_PacketInfoLine(text: 'Hash: ${packetDetails.packetHashHex}'),
|
||||||
),
|
if (packetDetails.pathLine != null)
|
||||||
const SizedBox(height: 10),
|
_PacketInfoLine(text: packetDetails.pathLine!),
|
||||||
Wrap(
|
if (packetDetails.pathHashLine != null)
|
||||||
spacing: 8,
|
_PacketInfoLine(text: packetDetails.pathHashLine!),
|
||||||
runSpacing: 8,
|
if (packetDetails.endpointLine != null)
|
||||||
children: [
|
_PacketInfoLine(text: packetDetails.endpointLine!),
|
||||||
_PacketMetaChip(
|
|
||||||
label: '${log.rawData.length} bytes',
|
|
||||||
onTap: () => _showPacketBytesSheet(context, log.rawData),
|
|
||||||
),
|
|
||||||
if (rxInfo?.rssiDbm != null)
|
|
||||||
_PacketMetaChip(label: 'RSSI ${rxInfo!.rssiDbm} dBm'),
|
|
||||||
if (rxInfo?.snrDb != null)
|
|
||||||
_PacketMetaChip(
|
|
||||||
label: 'SNR ${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
|
||||||
),
|
|
||||||
if (entry.hopCount != null)
|
|
||||||
_PacketMetaChip(label: '${entry.hopCount} hops'),
|
|
||||||
if (entry.isMultiHop)
|
|
||||||
const _PacketMetaChip(label: 'MULTI-HOP', emphasized: true),
|
|
||||||
if (originDistance != null)
|
if (originDistance != null)
|
||||||
_PacketMetaChip(label: 'Origin $originDistance'),
|
_PacketInfoLine(text: 'Origin: $originDistance'),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
),
|
||||||
Text(
|
|
||||||
routePreview,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: 'monospace',
|
|
||||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -748,36 +782,11 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
return '${diff.inMinutes}m ago';
|
return '${diff.inMinutes}m ago';
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _resolvedRoutePreview(
|
static String _formatClock(DateTime timestamp) {
|
||||||
BuildContext context,
|
final hour = timestamp.hour.toString().padLeft(2, '0');
|
||||||
LiveTrafficEntry entry,
|
final minute = timestamp.minute.toString().padLeft(2, '0');
|
||||||
) {
|
final second = timestamp.second.toString().padLeft(2, '0');
|
||||||
final route = entry.route;
|
return '$hour:$minute:$second';
|
||||||
if (route == null || route.hopHashes.isEmpty) {
|
|
||||||
return entry.routePreview;
|
|
||||||
}
|
|
||||||
|
|
||||||
final contactsProvider = _maybeProvider<ContactsProvider>(context);
|
|
||||||
final connectionProvider = _maybeProvider<ConnectionProvider>(context);
|
|
||||||
if (contactsProvider == null && connectionProvider == null) {
|
|
||||||
return entry.routePreview;
|
|
||||||
}
|
|
||||||
final ownLatLng = _ownLatLng(connectionProvider);
|
|
||||||
|
|
||||||
final resolvedLabels = route.hopHashes.map((hashHex) {
|
|
||||||
final resolved = LogRxRouteDecoder.resolveHash(
|
|
||||||
hashHex,
|
|
||||||
contacts: contactsProvider?.contacts ?? const <Contact>[],
|
|
||||||
ownPublicKey: connectionProvider?.deviceInfo.publicKey,
|
|
||||||
ownName:
|
|
||||||
connectionProvider?.deviceInfo.selfName ??
|
|
||||||
connectionProvider?.deviceInfo.displayName,
|
|
||||||
ownLatitude: ownLatLng?.latitude,
|
|
||||||
ownLongitude: ownLatLng?.longitude,
|
|
||||||
);
|
|
||||||
return _compactNodeLabel(resolved);
|
|
||||||
}).toList();
|
|
||||||
return resolvedLabels.join(' -> ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static String? _originDistanceLabel(
|
static String? _originDistanceLabel(
|
||||||
@@ -856,11 +865,16 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
final hex = chunk
|
final hex = chunk
|
||||||
.map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase())
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0').toUpperCase())
|
||||||
.join(' ');
|
.join(' ');
|
||||||
hexLines.add('${offset.toRadixString(16).padLeft(4, '0').toUpperCase()}: $hex');
|
hexLines.add(
|
||||||
|
'${offset.toRadixString(16).padLeft(4, '0').toUpperCase()}: $hex',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final ascii = data
|
final ascii = data
|
||||||
.map((byte) => (byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.')
|
.map(
|
||||||
|
(byte) =>
|
||||||
|
(byte >= 32 && byte <= 126) ? String.fromCharCode(byte) : '.',
|
||||||
|
)
|
||||||
.join();
|
.join();
|
||||||
|
|
||||||
return showModalBottomSheet<void>(
|
return showModalBottomSheet<void>(
|
||||||
@@ -886,10 +900,7 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
style: TextStyle(color: scheme.onSurfaceVariant),
|
style: TextStyle(color: scheme.onSurfaceVariant),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text('Hex', style: Theme.of(context).textTheme.titleSmall),
|
||||||
'Hex',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -909,10 +920,7 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text('ASCII', style: Theme.of(context).textTheme.titleSmall),
|
||||||
'ASCII',
|
|
||||||
style: Theme.of(context).textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Container(
|
Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
@@ -940,14 +948,182 @@ class _LiveTrafficCard extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static String _compactNodeLabel(ResolvedNodeHash node) {
|
static Future<void> _showTraceSheet(
|
||||||
if (node.isOwnNode) {
|
BuildContext context,
|
||||||
return node.label;
|
LiveTrafficEntry entry,
|
||||||
|
) {
|
||||||
|
final route = entry.route;
|
||||||
|
if (route == null || route.pathBytes.isEmpty) {
|
||||||
|
return Future.value();
|
||||||
}
|
}
|
||||||
if (node.matchCount > 0) {
|
return showModalBottomSheet<void>(
|
||||||
return node.label;
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) => MessageTraceSheet.packetPath(
|
||||||
|
packetPath: route.pathBytes,
|
||||||
|
descriptionOverride:
|
||||||
|
'Relay path from packet path bytes (${route.hopHashes.length} hop${route.hopHashes.length == 1 ? '' : 's'})',
|
||||||
|
noRelayMatchTextOverride:
|
||||||
|
'No named nodes could be matched for this packet path.',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _LiveTrafficPacketDetails {
|
||||||
|
final String title;
|
||||||
|
final String packetHashHex;
|
||||||
|
final String? pathLine;
|
||||||
|
final String? pathHashLine;
|
||||||
|
final String? endpointLine;
|
||||||
|
|
||||||
|
const _LiveTrafficPacketDetails({
|
||||||
|
required this.title,
|
||||||
|
required this.packetHashHex,
|
||||||
|
required this.pathLine,
|
||||||
|
required this.pathHashLine,
|
||||||
|
required this.endpointLine,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory _LiveTrafficPacketDetails.fromEntry(LiveTrafficEntry entry) {
|
||||||
|
final route = entry.route;
|
||||||
|
final payloadType = route?.payloadType;
|
||||||
|
final parsedPayload = _ParsedTrafficPayload.tryParse(
|
||||||
|
entry.log.rawData,
|
||||||
|
route,
|
||||||
|
);
|
||||||
|
final title = switch (payloadType) {
|
||||||
|
0x00 => 'FLOOD REQUEST',
|
||||||
|
0x01 => 'FLOOD RESPONSE',
|
||||||
|
0x02 => 'FLOOD TEXT',
|
||||||
|
0x03 => 'FLOOD ACK',
|
||||||
|
0x04 => 'FLOOD ADVERTISEMENT',
|
||||||
|
0x05 => 'FLOOD GROUP_TEXT',
|
||||||
|
0x06 => 'FLOOD GROUP_DATA',
|
||||||
|
0x07 => 'FLOOD ANON_REQUEST',
|
||||||
|
0x08 => 'FLOOD RETURNED_PATH',
|
||||||
|
0x09 => 'FLOOD TRACE_PATH',
|
||||||
|
0x0A => 'FLOOD MULTIPART',
|
||||||
|
0x0B => 'FLOOD CONTROL',
|
||||||
|
_ => entry.payloadLabel.toUpperCase(),
|
||||||
|
};
|
||||||
|
|
||||||
|
final hopHashes = route?.hopHashes ?? const <String>[];
|
||||||
|
final pathLine = hopHashes.isEmpty
|
||||||
|
? null
|
||||||
|
: 'Path: ${hopHashes.length} hop${hopHashes.length == 1 ? '' : 's'} [${hopHashes.join(',')}]';
|
||||||
|
final pathHashLine = route == null
|
||||||
|
? null
|
||||||
|
: 'Path Hashes: ${route.hashSize}-byte per hop';
|
||||||
|
|
||||||
|
return _LiveTrafficPacketDetails(
|
||||||
|
title: title,
|
||||||
|
packetHashHex: _packetHash(entry.log.rawData),
|
||||||
|
pathLine: pathLine,
|
||||||
|
pathHashLine: pathHashLine,
|
||||||
|
endpointLine: parsedPayload?.endpointLine,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static String _packetHash(List<int> bytes) {
|
||||||
|
final digest = md5.convert(bytes).bytes;
|
||||||
|
return digest
|
||||||
|
.take(8)
|
||||||
|
.map((byte) => byte.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join()
|
||||||
|
.toUpperCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ParsedTrafficPayload {
|
||||||
|
final String? endpointLine;
|
||||||
|
|
||||||
|
const _ParsedTrafficPayload({this.endpointLine});
|
||||||
|
|
||||||
|
static _ParsedTrafficPayload? tryParse(
|
||||||
|
List<int> rawData,
|
||||||
|
DecodedLogRxRoute? route,
|
||||||
|
) {
|
||||||
|
if (rawData.length < 5 ||
|
||||||
|
rawData.first != LiveTrafficSummary.logRxDataResponseCode) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return node.hexLabel;
|
|
||||||
|
final rawPacketData = rawData.sublist(3);
|
||||||
|
if (rawPacketData.length < 2) return null;
|
||||||
|
|
||||||
|
final header = rawPacketData[0];
|
||||||
|
final routeType = header & 0x03;
|
||||||
|
final payloadType = (header >> 2) & 0x0F;
|
||||||
|
|
||||||
|
var index = 1;
|
||||||
|
if (routeType == 0x00 || routeType == 0x03) {
|
||||||
|
if (rawPacketData.length < index + 5) return null;
|
||||||
|
index += 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rawPacketData.length <= index) return null;
|
||||||
|
final pathDescriptor = rawPacketData[index++];
|
||||||
|
final pathByteLen =
|
||||||
|
LogRxRouteDecoder.descriptorByteLength(pathDescriptor) ??
|
||||||
|
(pathDescriptor == 0xFF ? 0 : null);
|
||||||
|
if (pathByteLen == null || rawPacketData.length < index + pathByteLen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
index += pathByteLen;
|
||||||
|
final payload = rawPacketData.sublist(index);
|
||||||
|
final senderHash = route?.hopHashes.isNotEmpty == true
|
||||||
|
? route!.hopHashes.first
|
||||||
|
: null;
|
||||||
|
|
||||||
|
switch (payloadType) {
|
||||||
|
case 0x05:
|
||||||
|
case 0x06:
|
||||||
|
if (payload.isEmpty) return const _ParsedTrafficPayload();
|
||||||
|
return _ParsedTrafficPayload(
|
||||||
|
endpointLine:
|
||||||
|
'Channel Hash: ${payload.first.toRadixString(16).padLeft(2, '0').toUpperCase()}',
|
||||||
|
);
|
||||||
|
case 0x00:
|
||||||
|
case 0x01:
|
||||||
|
case 0x07:
|
||||||
|
if (payload.isEmpty) return const _ParsedTrafficPayload();
|
||||||
|
final destinationHash = payload.first
|
||||||
|
.toRadixString(16)
|
||||||
|
.padLeft(2, '0')
|
||||||
|
.toUpperCase();
|
||||||
|
return _ParsedTrafficPayload(
|
||||||
|
endpointLine: senderHash == null
|
||||||
|
? 'To: <$destinationHash>'
|
||||||
|
: 'From: <${senderHash.toUpperCase()}> To: <$destinationHash>',
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const _ParsedTrafficPayload();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PacketInfoLine extends StatelessWidget {
|
||||||
|
final String text;
|
||||||
|
|
||||||
|
const _PacketInfoLine({required this.text});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 4),
|
||||||
|
child: Text(
|
||||||
|
text,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Theme.of(context).colorScheme.onSurface,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,44 +1134,6 @@ class _GeoPoint {
|
|||||||
const _GeoPoint(this.latitude, this.longitude);
|
const _GeoPoint(this.latitude, this.longitude);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PacketMetaChip extends StatelessWidget {
|
|
||||||
final String label;
|
|
||||||
final bool emphasized;
|
|
||||||
final VoidCallback? onTap;
|
|
||||||
|
|
||||||
const _PacketMetaChip({
|
|
||||||
required this.label,
|
|
||||||
this.emphasized = false,
|
|
||||||
this.onTap,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
final scheme = Theme.of(context).colorScheme;
|
|
||||||
final color = emphasized ? scheme.primary : scheme.outline;
|
|
||||||
return InkWell(
|
|
||||||
onTap: onTap,
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: color.withValues(alpha: emphasized ? 0.12 : 0.08),
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
border: Border.all(color: color.withValues(alpha: 0.22)),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: color,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void openLiveTrafficScreen(BuildContext context, ConnectionProvider provider) {
|
void openLiveTrafficScreen(BuildContext context, ConnectionProvider provider) {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
|
|||||||
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 '../services/voice_codec_service.dart';
|
||||||
import '../utils/toast_logger.dart';
|
import '../utils/toast_logger.dart';
|
||||||
import '../utils/key_comparison.dart';
|
import '../utils/key_comparison.dart';
|
||||||
|
import '../utils/contact_sorting.dart';
|
||||||
import '../utils/voice_message_parser.dart';
|
import '../utils/voice_message_parser.dart';
|
||||||
import '../utils/image_message_parser.dart';
|
import '../utils/image_message_parser.dart';
|
||||||
import '../utils/tictactoe_message_parser.dart';
|
import '../utils/tictactoe_message_parser.dart';
|
||||||
@@ -47,6 +48,110 @@ class MessagesTab extends StatefulWidget {
|
|||||||
State<MessagesTab> createState() => _MessagesTabState();
|
State<MessagesTab> createState() => _MessagesTabState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _ComposerActionTile extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Color color;
|
||||||
|
final bool enabled;
|
||||||
|
final VoidCallback? onTap;
|
||||||
|
|
||||||
|
const _ComposerActionTile({
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.color,
|
||||||
|
this.enabled = true,
|
||||||
|
this.onTap,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final effectiveColor = enabled
|
||||||
|
? color
|
||||||
|
: colorScheme.onSurfaceVariant.withValues(alpha: 0.45);
|
||||||
|
|
||||||
|
return Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
onTap: enabled ? onTap : null,
|
||||||
|
child: Ink(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [
|
||||||
|
enabled
|
||||||
|
? colorScheme.surfaceContainerLow
|
||||||
|
: colorScheme.surfaceContainerHighest,
|
||||||
|
enabled
|
||||||
|
? effectiveColor.withValues(alpha: 0.08)
|
||||||
|
: colorScheme.surfaceContainerHigh,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: enabled
|
||||||
|
? effectiveColor.withValues(alpha: 0.16)
|
||||||
|
: colorScheme.outlineVariant.withValues(alpha: 0.16),
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: effectiveColor.withValues(alpha: enabled ? 0.08 : 0.03),
|
||||||
|
blurRadius: 14,
|
||||||
|
offset: const Offset(0, 8),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(12),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 38,
|
||||||
|
height: 38,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: effectiveColor.withValues(alpha: enabled ? 0.14 : 0.10),
|
||||||
|
borderRadius: BorderRadius.circular(14),
|
||||||
|
),
|
||||||
|
alignment: Alignment.center,
|
||||||
|
child: Icon(icon, color: effectiveColor, size: 20),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.titleSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: -0.2,
|
||||||
|
color: enabled
|
||||||
|
? colorScheme.onSurface
|
||||||
|
: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
height: 1.15,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _MessagesTabState extends State<MessagesTab> {
|
class _MessagesTabState extends State<MessagesTab> {
|
||||||
static const int _maxContactMessageBytes = 156;
|
static const int _maxContactMessageBytes = 156;
|
||||||
static const int _maxChannelMessageBytes = 127;
|
static const int _maxChannelMessageBytes = 127;
|
||||||
@@ -61,9 +166,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
Timer? _highlightTimer; // Timer for clearing message highlight
|
Timer? _highlightTimer; // Timer for clearing message highlight
|
||||||
Timer? _channelReadTimer;
|
Timer? _channelReadTimer;
|
||||||
String? _pendingChannelReadKey;
|
String? _pendingChannelReadKey;
|
||||||
TextEditingValue _lastComposerValue = const TextEditingValue();
|
|
||||||
bool _isMentionPickerOpen = false;
|
|
||||||
bool _suppressMentionTrigger = false;
|
bool _suppressMentionTrigger = false;
|
||||||
|
TextRange? _activeMentionRange;
|
||||||
|
String _mentionQuery = '';
|
||||||
|
List<Contact> _mentionSuggestions = const [];
|
||||||
|
|
||||||
// Message destination state
|
// Message destination state
|
||||||
String _destinationType =
|
String _destinationType =
|
||||||
@@ -92,8 +198,8 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
_lastComposerValue = _textController.value;
|
|
||||||
_textController.addListener(_handleComposerChanged);
|
_textController.addListener(_handleComposerChanged);
|
||||||
|
_focusNode.addListener(_handleFocusChanged);
|
||||||
// Load saved message destination
|
// Load saved message destination
|
||||||
_loadSavedDestination();
|
_loadSavedDestination();
|
||||||
_loadVoiceSettings();
|
_loadVoiceSettings();
|
||||||
@@ -126,6 +232,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
_channelReadTimer?.cancel();
|
_channelReadTimer?.cancel();
|
||||||
_voiceStreamSub?.cancel();
|
_voiceStreamSub?.cancel();
|
||||||
_voiceRecorder.dispose();
|
_voiceRecorder.dispose();
|
||||||
|
_focusNode.removeListener(_handleFocusChanged);
|
||||||
_textController.dispose();
|
_textController.dispose();
|
||||||
_focusNode.dispose();
|
_focusNode.dispose();
|
||||||
_scrollController.dispose();
|
_scrollController.dispose();
|
||||||
@@ -161,9 +268,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
|
|
||||||
void _scrollToMessage(String messageId) {
|
void _scrollToMessage(String messageId) {
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
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) {
|
if (messageIndex != -1 && _scrollController.hasClients) {
|
||||||
// Calculate position - accounting for reverse list
|
// Calculate position - accounting for reverse list
|
||||||
@@ -195,25 +306,13 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _handleComposerChanged() {
|
void _handleComposerChanged() {
|
||||||
final previousValue = _lastComposerValue;
|
|
||||||
final currentValue = _textController.value;
|
|
||||||
_lastComposerValue = currentValue;
|
|
||||||
|
|
||||||
_updateCharacterCount();
|
_updateCharacterCount();
|
||||||
|
|
||||||
if (_suppressMentionTrigger || _isMentionPickerOpen) {
|
if (_suppressMentionTrigger) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final mentionTriggerRange = _getMentionTriggerRange(
|
_updateMentionSuggestions(_textController.value);
|
||||||
previousValue: previousValue,
|
|
||||||
currentValue: currentValue,
|
|
||||||
);
|
|
||||||
if (mentionTriggerRange == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
unawaited(_showMentionSelectorForRange(mentionTriggerRange));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateCharacterCount() {
|
void _updateCharacterCount() {
|
||||||
@@ -222,43 +321,112 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
TextRange? _getMentionTriggerRange({
|
void _handleFocusChanged() {
|
||||||
required TextEditingValue previousValue,
|
if (!_focusNode.hasFocus) {
|
||||||
required TextEditingValue currentValue,
|
_clearMentionSuggestions();
|
||||||
}) {
|
return;
|
||||||
if (!previousValue.selection.isValid ||
|
}
|
||||||
!currentValue.selection.isValid ||
|
|
||||||
!previousValue.selection.isCollapsed ||
|
if (_suppressMentionTrigger) {
|
||||||
!currentValue.selection.isCollapsed) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
final previousOffset = previousValue.selection.baseOffset;
|
final cursorOffset = value.selection.baseOffset;
|
||||||
final currentOffset = currentValue.selection.baseOffset;
|
if (cursorOffset <= 0) {
|
||||||
if (previousOffset < 0 || currentOffset < 0) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue.text.length != previousValue.text.length + 1 ||
|
final text = value.text;
|
||||||
currentOffset != previousOffset + 1) {
|
final triggerStart = text.lastIndexOf('@', cursorOffset - 1);
|
||||||
|
if (triggerStart == -1) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue.text.substring(0, previousOffset) !=
|
final leadingChar = triggerStart > 0 ? text[triggerStart - 1] : null;
|
||||||
previousValue.text.substring(0, previousOffset)) {
|
if (leadingChar != null && !RegExp(r'[\s\(\[\{]').hasMatch(leadingChar)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue.text.substring(currentOffset) !=
|
final query = text.substring(triggerStart + 1, cursorOffset);
|
||||||
previousValue.text.substring(previousOffset)) {
|
if (query.contains(RegExp(r'[\s@\[\]\n\r]'))) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentValue.text[previousOffset] != '@') {
|
return TextRange(start: triggerStart, end: cursorOffset);
|
||||||
return null;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return TextRange(start: previousOffset, end: currentOffset);
|
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 =>
|
int get _maxMessageBytes =>
|
||||||
@@ -345,16 +513,33 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
final messagesProvider = context.read<MessagesProvider>();
|
final messagesProvider = context.read<MessagesProvider>();
|
||||||
|
|
||||||
// Filter contacts by type
|
final contacts = List<Contact>.from(contactsProvider.chatContacts)
|
||||||
final contacts = contactsProvider.contacts
|
..sort((a, b) {
|
||||||
.where((c) => c.type == ContactType.chat)
|
final primary = compareContactsByFavouriteThenLastSeen(a, b);
|
||||||
.toList();
|
if (primary != 0) {
|
||||||
final rooms = contactsProvider.contacts
|
return primary;
|
||||||
.where((c) => c.type == ContactType.room)
|
}
|
||||||
.toList();
|
|
||||||
final channels = contactsProvider.contacts
|
return compareContactsByDisplayName(a, b);
|
||||||
.where((c) => c.type == ContactType.channel)
|
});
|
||||||
.toList();
|
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(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
@@ -378,56 +563,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showMentionSelectorForRange(TextRange triggerRange) async {
|
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
|
||||||
final contacts = contactsProvider.contacts
|
|
||||||
.where((contact) => contact.type == ContactType.chat)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
if (contacts.isEmpty || !mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isMentionPickerOpen = true;
|
|
||||||
Contact? selectedContact;
|
|
||||||
|
|
||||||
await showModalBottomSheet<void>(
|
|
||||||
context: context,
|
|
||||||
isScrollControlled: true,
|
|
||||||
backgroundColor: Colors.transparent,
|
|
||||||
builder: (context) => RecipientSelectorSheet(
|
|
||||||
contacts: contacts,
|
|
||||||
rooms: const [],
|
|
||||||
channels: const [],
|
|
||||||
unreadCount: 0,
|
|
||||||
unreadCountsByPublicKey: {
|
|
||||||
for (final contact in contacts) contact.publicKeyHex: 0,
|
|
||||||
},
|
|
||||||
currentDestinationType: null,
|
|
||||||
currentRecipientPublicKey: null,
|
|
||||||
showAllOption: false,
|
|
||||||
onSelect: (_, recipient) {
|
|
||||||
selectedContact = recipient;
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
_isMentionPickerOpen = false;
|
|
||||||
|
|
||||||
if (!mounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedContact != null) {
|
|
||||||
_insertReplyMention(
|
|
||||||
selectedContact!.displayName,
|
|
||||||
replacementRange: triggerRange,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
_focusNode.requestFocus();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle recipient selection
|
/// Handle recipient selection
|
||||||
Future<void> _onRecipientSelected(String type, Contact? recipient) async {
|
Future<void> _onRecipientSelected(String type, Contact? recipient) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -489,15 +624,23 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
selection: TextSelection.collapsed(offset: nextOffset),
|
selection: TextSelection.collapsed(offset: nextOffset),
|
||||||
composing: TextRange.empty,
|
composing: TextRange.empty,
|
||||||
);
|
);
|
||||||
_lastComposerValue = _textController.value;
|
|
||||||
_suppressMentionTrigger = false;
|
_suppressMentionTrigger = false;
|
||||||
|
_clearMentionSuggestions();
|
||||||
_enforceMessageByteLimit();
|
_enforceMessageByteLimit();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _selectMention(Contact contact) {
|
||||||
|
_insertReplyMention(
|
||||||
|
contact.displayName,
|
||||||
|
replacementRange: _activeMentionRange,
|
||||||
|
);
|
||||||
|
_focusNode.requestFocus();
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _replyToMessage(Message message) async {
|
Future<void> _replyToMessage(Message message) async {
|
||||||
final l10n = AppLocalizations.of(context)!;
|
|
||||||
final contactsProvider = context.read<ContactsProvider>();
|
final contactsProvider = context.read<ContactsProvider>();
|
||||||
Contact? senderContact;
|
Contact? senderContact;
|
||||||
|
String? senderDisplayName;
|
||||||
|
|
||||||
String destinationType;
|
String destinationType;
|
||||||
Contact? recipient;
|
Contact? recipient;
|
||||||
@@ -513,7 +656,10 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}).firstOrNull;
|
}).firstOrNull;
|
||||||
|
|
||||||
if (recipient == null) {
|
if (recipient == null) {
|
||||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
ToastLogger.error(
|
||||||
|
context,
|
||||||
|
AppLocalizations.of(context)!.cannotReplyContactNotFound,
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -522,28 +668,54 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
if (senderPrefix != null && senderPrefix.length >= 6) {
|
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||||
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
|
senderContact = contactsProvider.findContactByPrefix(senderPrefix);
|
||||||
}
|
}
|
||||||
|
senderDisplayName =
|
||||||
|
senderContact?.displayName ?? message.senderName?.trim();
|
||||||
} else {
|
} else {
|
||||||
final senderPrefix = message.senderPublicKeyPrefix;
|
final roomRecipient = message.recipientPublicKey == null
|
||||||
if (senderPrefix == null || senderPrefix.length < 6) {
|
? null
|
||||||
ToastLogger.error(context, l10n.cannotReplySenderMissing);
|
: contactsProvider.findContactByKey(message.recipientPublicKey!);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
recipient = contactsProvider.findContactByPrefix(senderPrefix);
|
if (roomRecipient?.isRoom == true) {
|
||||||
if (recipient == null) {
|
destinationType = MessageDestinationPreferences.destinationTypeRoom;
|
||||||
ToastLogger.error(context, l10n.cannotReplyContactNotFound);
|
recipient = roomRecipient;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
destinationType = recipient.isRoom
|
final senderPrefix = message.senderPublicKeyPrefix;
|
||||||
? MessageDestinationPreferences.destinationTypeRoom
|
if (senderPrefix != null && senderPrefix.length >= 6) {
|
||||||
: MessageDestinationPreferences.destinationTypeContact;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await _onRecipientSelected(destinationType, recipient);
|
await _onRecipientSelected(destinationType, recipient);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (message.isChannelMessage && senderContact != null) {
|
if ((message.isChannelMessage || recipient?.isRoom == true) &&
|
||||||
_insertReplyMention(senderContact.displayName);
|
senderDisplayName != null &&
|
||||||
|
senderDisplayName.isNotEmpty) {
|
||||||
|
_insertReplyMention(senderDisplayName);
|
||||||
}
|
}
|
||||||
_focusNode.requestFocus();
|
_focusNode.requestFocus();
|
||||||
}
|
}
|
||||||
@@ -599,7 +771,6 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_textController.clear();
|
_textController.clear();
|
||||||
_focusNode.unfocus();
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check destination type and send accordingly
|
// Check destination type and send accordingly
|
||||||
@@ -1334,7 +1505,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
final decision = await showDialog<bool>(
|
final decision = await showDialog<bool>(
|
||||||
context: context,
|
context: context,
|
||||||
builder: (dialogContext) => AlertDialog(
|
builder: (dialogContext) => AlertDialog(
|
||||||
title: const Text('Send to Public Channel?'),
|
title: Text(AppLocalizations.of(context)!.sendToPublicChannel),
|
||||||
content: Text(
|
content: Text(
|
||||||
'You are about to send $mediaType to the Public Channel. '
|
'You are about to send $mediaType to the Public Channel. '
|
||||||
'This is not advised because everyone on the mesh may receive it. '
|
'This is not advised because everyone on the mesh may receive it. '
|
||||||
@@ -1347,7 +1518,7 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
),
|
),
|
||||||
FilledButton(
|
FilledButton(
|
||||||
onPressed: () => Navigator.of(dialogContext).pop(true),
|
onPressed: () => Navigator.of(dialogContext).pop(true),
|
||||||
child: const Text('Send anyway'),
|
child: Text(AppLocalizations.of(context)!.sendAnyway),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -1401,81 +1572,207 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
void _showComposerActions() {
|
void _showComposerActions() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
builder: (sheetContext) {
|
builder: (sheetContext) {
|
||||||
|
final theme = Theme.of(sheetContext);
|
||||||
|
final colorScheme = theme.colorScheme;
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
|
||||||
|
Future<void> runAction(Future<void> Function() action) async {
|
||||||
|
await _runAfterSheetDismissal(sheetContext, action);
|
||||||
|
}
|
||||||
|
|
||||||
return SafeArea(
|
return SafeArea(
|
||||||
child: Column(
|
child: ConstrainedBox(
|
||||||
mainAxisSize: MainAxisSize.min,
|
constraints: BoxConstraints(
|
||||||
children: [
|
maxHeight: MediaQuery.of(sheetContext).size.height * 0.72,
|
||||||
ListTile(
|
),
|
||||||
leading: const Icon(Icons.search),
|
child: Container(
|
||||||
title: const Text('Search messages'),
|
margin: const EdgeInsets.fromLTRB(12, 0, 12, 12),
|
||||||
onTap: () async {
|
decoration: BoxDecoration(
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
gradient: LinearGradient(
|
||||||
_showFilteredMessageSearch();
|
begin: Alignment.topCenter,
|
||||||
});
|
end: Alignment.bottomCenter,
|
||||||
},
|
colors: [
|
||||||
),
|
colorScheme.surface,
|
||||||
ListTile(
|
colorScheme.surfaceContainerLow,
|
||||||
leading: const Icon(Icons.add_location_alt),
|
],
|
||||||
title: Text(AppLocalizations.of(context)!.sendSarMarker),
|
|
||||||
onTap: () async {
|
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
|
||||||
_showSarDialog();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
if (_voiceSupported)
|
|
||||||
ListTile(
|
|
||||||
enabled: !_isSendingVoice,
|
|
||||||
leading: Icon(_isRecording ? Icons.stop : Icons.mic),
|
|
||||||
title: Text(_isRecording ? 'Stop recording' : 'Record voice'),
|
|
||||||
onTap: _isSendingVoice
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
|
||||||
if (_isRecording) {
|
|
||||||
await _stopAndSendVoice();
|
|
||||||
} else {
|
|
||||||
await _startVoiceRecording();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
borderRadius: const BorderRadius.vertical(
|
||||||
enabled: !_isSendingImage,
|
top: Radius.circular(32),
|
||||||
leading: const Icon(Icons.photo_library),
|
bottom: Radius.circular(28),
|
||||||
title: const Text('Send image from gallery'),
|
),
|
||||||
onTap: _isSendingImage
|
boxShadow: [
|
||||||
? null
|
BoxShadow(
|
||||||
: () async {
|
color: colorScheme.shadow.withValues(alpha: 0.14),
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
blurRadius: 28,
|
||||||
await _pickAndSendImage(source: ImageSource.gallery);
|
offset: const Offset(0, 10),
|
||||||
});
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 18),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Center(
|
||||||
|
child: Container(
|
||||||
|
width: 44,
|
||||||
|
height: 4,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.outlineVariant,
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'More actions',
|
||||||
|
style: theme.textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: -0.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'Search, share, or start something from this chat.',
|
||||||
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: colorScheme.surfaceContainerHigh,
|
||||||
|
borderRadius: BorderRadius.circular(18),
|
||||||
|
border: Border.all(
|
||||||
|
color: colorScheme.outlineVariant.withValues(
|
||||||
|
alpha: 0.18,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: IconButton(
|
||||||
|
onPressed: () => Navigator.pop(sheetContext),
|
||||||
|
tooltip: l10n.close,
|
||||||
|
icon: const Icon(Icons.close_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 18),
|
||||||
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final actions = <Widget>[
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: Icons.search_rounded,
|
||||||
|
title: l10n.searchMessages,
|
||||||
|
subtitle: 'Find text in the current conversation',
|
||||||
|
color: const Color(0xFF2B6CB0),
|
||||||
|
onTap: () => runAction(() async {
|
||||||
|
_showFilteredMessageSearch();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: Icons.add_location_alt_rounded,
|
||||||
|
title: l10n.sendSarMarker,
|
||||||
|
subtitle: 'Share a marker with coordinates',
|
||||||
|
color: const Color(0xFFB45309),
|
||||||
|
onTap: () => runAction(() async {
|
||||||
|
_showSarDialog();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
if (_voiceSupported)
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: _isRecording
|
||||||
|
? Icons.stop_rounded
|
||||||
|
: Icons.mic_rounded,
|
||||||
|
title: _isRecording
|
||||||
|
? 'Stop recording'
|
||||||
|
: 'Record voice',
|
||||||
|
subtitle: _isSendingVoice
|
||||||
|
? 'Voice message is sending'
|
||||||
|
: _isRecording
|
||||||
|
? 'Finish and send your clip'
|
||||||
|
: 'Capture and send a voice note',
|
||||||
|
color: const Color(0xFF7C3AED),
|
||||||
|
enabled: !_isSendingVoice,
|
||||||
|
onTap: !_isSendingVoice
|
||||||
|
? () => runAction(() async {
|
||||||
|
if (_isRecording) {
|
||||||
|
await _stopAndSendVoice();
|
||||||
|
} else {
|
||||||
|
await _startVoiceRecording();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: Icons.photo_library_rounded,
|
||||||
|
title: l10n.sendImageFromGallery,
|
||||||
|
subtitle: 'Choose an image from your library',
|
||||||
|
color: const Color(0xFF0F766E),
|
||||||
|
enabled: !_isSendingImage,
|
||||||
|
onTap: !_isSendingImage
|
||||||
|
? () => runAction(() async {
|
||||||
|
await _pickAndSendImage(
|
||||||
|
source: ImageSource.gallery,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: Icons.camera_alt_rounded,
|
||||||
|
title: l10n.takePhoto,
|
||||||
|
subtitle: 'Capture something right now',
|
||||||
|
color: const Color(0xFF2563EB),
|
||||||
|
enabled: !_isSendingImage,
|
||||||
|
onTap: !_isSendingImage
|
||||||
|
? () => runAction(() async {
|
||||||
|
await _pickAndSendImage(
|
||||||
|
source: ImageSource.camera,
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
_ComposerActionTile(
|
||||||
|
icon: Icons.grid_3x3_rounded,
|
||||||
|
title: l10n.startTictactoe,
|
||||||
|
subtitle: l10n.dmOnly,
|
||||||
|
color: const Color(0xFFBE185D),
|
||||||
|
onTap: () => runAction(() async {
|
||||||
|
await _startTicTacToeGame();
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
return GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
itemCount: actions.length,
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 2,
|
||||||
|
mainAxisSpacing: 12,
|
||||||
|
crossAxisSpacing: 12,
|
||||||
|
mainAxisExtent: 126,
|
||||||
|
),
|
||||||
|
itemBuilder: (context, index) => actions[index],
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
ListTile(
|
),
|
||||||
enabled: !_isSendingImage,
|
|
||||||
leading: const Icon(Icons.camera_alt),
|
|
||||||
title: const Text('Take photo'),
|
|
||||||
onTap: _isSendingImage
|
|
||||||
? null
|
|
||||||
: () async {
|
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
|
||||||
await _pickAndSendImage(source: ImageSource.camera);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
ListTile(
|
|
||||||
leading: const Icon(Icons.grid_3x3),
|
|
||||||
title: const Text('Start Tic-Tac-Toe'),
|
|
||||||
subtitle: const Text('DM only'),
|
|
||||||
onTap: () async {
|
|
||||||
await _runAfterSheetDismissal(sheetContext, () async {
|
|
||||||
await _startTicTacToeGame();
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1978,12 +2275,19 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
void _handleMessageTap(Message message) {
|
void _handleMessageTap(Message message) {
|
||||||
if (widget.onNavigateToMap == null) return;
|
if (widget.onNavigateToMap == null) return;
|
||||||
|
|
||||||
if (message.isSarMarker && message.sarGpsCoordinates != null) {
|
if (message.isSarMarker) {
|
||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
mapProvider.navigateToLocation(
|
final marker = message.toSarMarker();
|
||||||
location: message.sarGpsCoordinates!,
|
if (marker == null) {
|
||||||
zoom: 15.0,
|
return;
|
||||||
);
|
}
|
||||||
|
final error = mapProvider.navigateToSarMarker(marker);
|
||||||
|
if (error != null && mounted) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text(error)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
widget.onNavigateToMap?.call();
|
widget.onNavigateToMap?.call();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1992,7 +2296,16 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
|
debugPrint('🗺️ [MessagesTab] Drawing tapped! ID: ${message.drawingId}');
|
||||||
final mapProvider = context.read<MapProvider>();
|
final mapProvider = context.read<MapProvider>();
|
||||||
final drawingProvider = context.read<DrawingProvider>();
|
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();
|
widget.onNavigateToMap?.call();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2002,7 +2315,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
return Consumer<MessagesProvider>(
|
return Consumer<MessagesProvider>(
|
||||||
builder: (context, messagesProvider, child) {
|
builder: (context, messagesProvider, child) {
|
||||||
_syncChannelAutoReadTimer(messagesProvider);
|
_syncChannelAutoReadTimer(messagesProvider);
|
||||||
final messages = _getFilteredMessages(messagesProvider);
|
final messages = messagesProvider.buildDisplayMessages(
|
||||||
|
_getFilteredMessages(messagesProvider),
|
||||||
|
);
|
||||||
final bottomInset = MediaQuery.of(context).viewPadding.bottom;
|
final bottomInset = MediaQuery.of(context).viewPadding.bottom;
|
||||||
final composerBottomPadding = bottomInset > 0 ? 2.0 : 10.0;
|
final composerBottomPadding = bottomInset > 0 ? 2.0 : 10.0;
|
||||||
|
|
||||||
@@ -2040,6 +2355,9 @@ class _MessagesTabState extends State<MessagesTab> {
|
|||||||
bottomPadding: composerBottomPadding,
|
bottomPadding: composerBottomPadding,
|
||||||
destinationLabel: _getDestinationLabel(),
|
destinationLabel: _getDestinationLabel(),
|
||||||
destinationAvatar: _buildDestinationAvatar(context),
|
destinationAvatar: _buildDestinationAvatar(context),
|
||||||
|
mentionSuggestions: _mentionSuggestions,
|
||||||
|
mentionQuery: _mentionQuery,
|
||||||
|
onMentionSelected: _selectMention,
|
||||||
onShowComposerActions: _showComposerActions,
|
onShowComposerActions: _showComposerActions,
|
||||||
onShowRecipientSelector: _showRecipientSelector,
|
onShowRecipientSelector: _showRecipientSelector,
|
||||||
onStartVoiceRecording: _startVoiceRecording,
|
onStartVoiceRecording: _startVoiceRecording,
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -108,7 +108,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
if (context.mounted) {
|
if (context.mounted) {
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
).showSnackBar(const SnackBar(content: Text('No logs to export')));
|
).showSnackBar(SnackBar(content: Text(AppLocalizations.of(context)!.noLogsToExport)));
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -155,8 +155,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
void _copyToClipboard(BuildContext context, BlePacketLog log) {
|
||||||
Clipboard.setData(ClipboardData(text: log.hexData));
|
Clipboard.setData(ClipboardData(text: log.hexData));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Hex data copied to clipboard'),
|
content: Text(AppLocalizations.of(context)!.hexDataCopiedToClipboard),
|
||||||
duration: Duration(seconds: 1),
|
duration: Duration(seconds: 1),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -184,7 +184,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
setState(() {});
|
setState(() {});
|
||||||
if (parentContext.mounted) {
|
if (parentContext.mounted) {
|
||||||
ScaffoldMessenger.of(parentContext).showSnackBar(
|
ScaffoldMessenger.of(parentContext).showSnackBar(
|
||||||
const SnackBar(content: Text('Packet logs cleared')),
|
SnackBar(content: Text(AppLocalizations.of(context)!.packetLogsCleared)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -205,7 +205,7 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
title: Column(
|
title: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text('BLE Packet Logs'),
|
Text(AppLocalizations.of(context)!.blePacketLogs),
|
||||||
Text(
|
Text(
|
||||||
'${logs.length} packets',
|
'${logs.length} packets',
|
||||||
style: Theme.of(context).textTheme.bodySmall,
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
@@ -316,23 +316,23 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
icon: const Icon(Icons.share),
|
icon: const Icon(Icons.share),
|
||||||
tooltip: 'Export logs',
|
tooltip: 'Export logs',
|
||||||
itemBuilder: (context) => [
|
itemBuilder: (context) => [
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'csv',
|
value: 'csv',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.table_chart),
|
const Icon(Icons.table_chart),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Export as CSV'),
|
Text(AppLocalizations.of(context)!.exportAsCsv),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'txt',
|
value: 'txt',
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(Icons.text_snippet),
|
const Icon(Icons.text_snippet),
|
||||||
SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Text('Export as Text'),
|
Text(AppLocalizations.of(context)!.exportAsText),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -411,8 +411,8 @@ class _PacketLogScreenState extends State<PacketLogScreen> {
|
|||||||
_filterDirection = null;
|
_filterDirection = null;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
icon: const Icon(Icons.clear_all),
|
icon: Icon(Icons.clear_all),
|
||||||
label: const Text('Clear filters'),
|
label: Text(AppLocalizations.of(context)!.clearFilters),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
@@ -519,24 +519,24 @@ class _PacketLogCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: isRx ? Icons.call_received : Icons.call_made,
|
icon: isRx ? Icons.call_received : Icons.call_made,
|
||||||
label: 'Direction',
|
label: AppLocalizations.of(context)!.direction,
|
||||||
value: isRx ? 'RX' : 'TX',
|
value: isRx ? 'RX' : 'TX',
|
||||||
accent: directionColor,
|
accent: directionColor,
|
||||||
),
|
),
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.data_object,
|
icon: Icons.data_object,
|
||||||
label: 'Size',
|
label: AppLocalizations.of(context)!.size,
|
||||||
value: '${log.rawData.length} bytes',
|
value: '${log.rawData.length} bytes',
|
||||||
),
|
),
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.schedule,
|
icon: Icons.schedule,
|
||||||
label: 'Captured',
|
label: AppLocalizations.of(context)!.captured,
|
||||||
value: _formatTimestamp(log.timestamp),
|
value: _formatTimestamp(log.timestamp),
|
||||||
),
|
),
|
||||||
if (log.responseCode != null)
|
if (log.responseCode != null)
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.sell,
|
icon: Icons.sell,
|
||||||
label: 'Opcode',
|
label: AppLocalizations.of(context)!.opcode,
|
||||||
value: log.opcodeName,
|
value: log.opcodeName,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -565,7 +565,7 @@ class _PacketLogCard extends StatelessWidget {
|
|||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
if (rxInfo?.rssiDbm != null)
|
if (rxInfo?.rssiDbm != null)
|
||||||
_SignalMeter(
|
_SignalMeter(
|
||||||
label: 'RSSI',
|
label: AppLocalizations.of(context)!.rssi,
|
||||||
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
valueLabel: '${rxInfo!.rssiDbm} dBm',
|
||||||
normalized: _normalizeRssi(
|
normalized: _normalizeRssi(
|
||||||
rxInfo.rssiDbm!.toDouble(),
|
rxInfo.rssiDbm!.toDouble(),
|
||||||
@@ -573,9 +573,9 @@ class _PacketLogCard extends StatelessWidget {
|
|||||||
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
color: _rssiColor(rxInfo.rssiDbm!.toDouble()),
|
||||||
),
|
),
|
||||||
if (rxInfo?.snrDb != null) ...[
|
if (rxInfo?.snrDb != null) ...[
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
_SignalMeter(
|
_SignalMeter(
|
||||||
label: 'SNR',
|
label: AppLocalizations.of(context)!.snr,
|
||||||
valueLabel:
|
valueLabel:
|
||||||
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
'${rxInfo!.snrDb!.toStringAsFixed(1)} dB',
|
||||||
normalized: _normalizeSnr(rxInfo.snrDb!),
|
normalized: _normalizeSnr(rxInfo.snrDb!),
|
||||||
@@ -777,13 +777,7 @@ class _DecodedRouteSection extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
.toList();
|
.toList();
|
||||||
final originalSender = resolvedPath.isEmpty ? null : resolvedPath.first;
|
return _RouteSection(route: decodedRoute, path: resolvedPath);
|
||||||
|
|
||||||
return _RouteSection(
|
|
||||||
route: decodedRoute,
|
|
||||||
path: resolvedPath,
|
|
||||||
originalSender: originalSender,
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -827,13 +821,8 @@ class _DecodedRouteSection extends StatelessWidget {
|
|||||||
class _RouteSection extends StatelessWidget {
|
class _RouteSection extends StatelessWidget {
|
||||||
final DecodedLogRxRoute route;
|
final DecodedLogRxRoute route;
|
||||||
final List<ResolvedNodeHash> path;
|
final List<ResolvedNodeHash> path;
|
||||||
final ResolvedNodeHash? originalSender;
|
|
||||||
|
|
||||||
const _RouteSection({
|
const _RouteSection({required this.route, required this.path});
|
||||||
required this.route,
|
|
||||||
required this.path,
|
|
||||||
required this.originalSender,
|
|
||||||
});
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -865,26 +854,20 @@ class _RouteSection extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.route,
|
icon: Icons.route,
|
||||||
label: 'Payload',
|
label: AppLocalizations.of(context)!.payload,
|
||||||
value: _payloadTypeLabel(route.payloadType),
|
value: _payloadTypeLabel(route.payloadType),
|
||||||
),
|
),
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.hub,
|
icon: Icons.hub,
|
||||||
label: 'Hops',
|
label: AppLocalizations.of(context)!.hops,
|
||||||
value: '${route.hopCount}',
|
value: '${route.hopCount}',
|
||||||
),
|
),
|
||||||
_FactCard(
|
_FactCard(
|
||||||
icon: Icons.tag,
|
icon: Icons.tag,
|
||||||
label: 'Hash size',
|
label: AppLocalizations.of(context)!.hashSize,
|
||||||
value:
|
value:
|
||||||
'${route.hashSize} byte${route.hashSize == 1 ? '' : 's'}',
|
'${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),
|
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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,6 +10,7 @@ import '../models/contact.dart';
|
|||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../providers/contacts_provider.dart';
|
import '../providers/contacts_provider.dart';
|
||||||
import '../services/mesh_map_nodes_service.dart';
|
import '../services/mesh_map_nodes_service.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class RepeatersMapScreen extends StatefulWidget {
|
class RepeatersMapScreen extends StatefulWidget {
|
||||||
const RepeatersMapScreen({super.key});
|
const RepeatersMapScreen({super.key});
|
||||||
@@ -191,18 +192,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
context,
|
context,
|
||||||
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
).textTheme.titleLarge?.copyWith(fontWeight: FontWeight.w700),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
SizedBox(height: 12),
|
||||||
_InfoLine(
|
_InfoLine(
|
||||||
label: 'Public key',
|
label: AppLocalizations.of(context)!.publicKeyLabel,
|
||||||
value: repeater.publicKey,
|
value: repeater.publicKey,
|
||||||
monospace: true,
|
monospace: true,
|
||||||
),
|
),
|
||||||
_InfoLine(
|
_InfoLine(
|
||||||
label: 'Coordinates',
|
label: AppLocalizations.of(context)!.coordinates,
|
||||||
value:
|
value:
|
||||||
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
'${repeater.latitude.toStringAsFixed(5)}, ${repeater.longitude.toStringAsFixed(5)}',
|
||||||
),
|
),
|
||||||
_InfoLine(label: 'Source', value: repeater.sourceLabel),
|
_InfoLine(label: AppLocalizations.of(context)!.source, value: repeater.sourceLabel),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
if (canAdd)
|
if (canAdd)
|
||||||
SizedBox(
|
SizedBox(
|
||||||
@@ -233,8 +234,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
child: FilledButton.icon(
|
child: FilledButton.icon(
|
||||||
onPressed: null,
|
onPressed: null,
|
||||||
icon: const Icon(Icons.check_circle),
|
icon: Icon(Icons.check_circle),
|
||||||
label: const Text('Already in contacts'),
|
label: Text(AppLocalizations.of(context)!.alreadyInContacts),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -253,8 +254,8 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
if (!connectionProvider.deviceInfo.isConnected) {
|
if (!connectionProvider.deviceInfo.isConnected) {
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Connect to a device before adding contacts'),
|
content: Text(AppLocalizations.of(context)!.connectToADeviceBeforeAddingContacts),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -366,7 +367,7 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text('Repeaters Map'),
|
title: Text(AppLocalizations.of(context)!.repeatersMap),
|
||||||
actions: [
|
actions: [
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: _isLoading
|
onPressed: _isLoading
|
||||||
@@ -446,18 +447,18 @@ class _RepeatersMapScreenState extends State<RepeatersMapScreen> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: const [
|
children: [
|
||||||
_LegendRow(
|
_LegendRow(
|
||||||
color: Color(0xFF7C3AED),
|
color: const Color(0xFF7C3AED),
|
||||||
label: 'From contacts',
|
label: AppLocalizations.of(context)!.fromContacts,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_LegendRow(
|
_LegendRow(
|
||||||
color: Color(0xFFE8681D),
|
color: const Color(0xFFE8681D),
|
||||||
label: 'Online only',
|
label: AppLocalizations.of(context)!.onlineOnly,
|
||||||
),
|
),
|
||||||
SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
_LegendRow(color: Color(0xFF1B8F4F), label: 'In both'),
|
_LegendRow(color: const Color(0xFF1B8F4F), label: AppLocalizations.of(context)!.inBoth),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'import',
|
value: 'import',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.download),
|
leading: Icon(Icons.download),
|
||||||
title: Text(l10n.importFromClipboard),
|
title: Text(l10n.importFromClipboard),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
@@ -248,7 +248,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'export',
|
value: 'export',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.upload),
|
leading: Icon(Icons.upload),
|
||||||
title: Text(l10n.exportToClipboard),
|
title: Text(l10n.exportToClipboard),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
@@ -257,7 +257,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
value: 'reset',
|
value: 'reset',
|
||||||
child: ListTile(
|
child: ListTile(
|
||||||
leading: const Icon(Icons.restart_alt),
|
leading: Icon(Icons.restart_alt),
|
||||||
title: Text(l10n.resetToDefaults),
|
title: Text(l10n.resetToDefaults),
|
||||||
contentPadding: EdgeInsets.zero,
|
contentPadding: EdgeInsets.zero,
|
||||||
),
|
),
|
||||||
@@ -283,14 +283,14 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
size: 64,
|
size: 64,
|
||||||
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
color: colorScheme.onSurface.withValues(alpha: 0.3),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
l10n.noTemplates,
|
l10n.noTemplates,
|
||||||
style: theme.textTheme.titleMedium?.copyWith(
|
style: theme.textTheme.titleMedium?.copyWith(
|
||||||
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
color: colorScheme.onSurface.withValues(alpha: 0.6),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
l10n.tapAddToCreate,
|
l10n.tapAddToCreate,
|
||||||
style: theme.textTheme.bodyMedium?.copyWith(
|
style: theme.textTheme.bodyMedium?.copyWith(
|
||||||
@@ -317,7 +317,7 @@ class _SarTemplateManagementScreenState extends State<SarTemplateManagementScree
|
|||||||
),
|
),
|
||||||
floatingActionButton: FloatingActionButton.extended(
|
floatingActionButton: FloatingActionButton.extended(
|
||||||
onPressed: _addTemplate,
|
onPressed: _addTemplate,
|
||||||
icon: const Icon(Icons.add),
|
icon: Icon(Icons.add),
|
||||||
label: Text(l10n.addTemplate),
|
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 '../models/device_info.dart';
|
||||||
import '../providers/connection_provider.dart';
|
import '../providers/connection_provider.dart';
|
||||||
import '../widgets/device/spectrum_scan_panel.dart';
|
import '../widgets/device/spectrum_scan_panel.dart';
|
||||||
|
import '../l10n/app_localizations.dart';
|
||||||
|
|
||||||
class SpectrumScanScreen extends StatefulWidget {
|
class SpectrumScanScreen extends StatefulWidget {
|
||||||
const SpectrumScanScreen({super.key});
|
const SpectrumScanScreen({super.key});
|
||||||
@@ -323,8 +324,8 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
|||||||
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
final mergedCandidates = _mergeSectorCandidates(sectorCandidates);
|
||||||
if (mergedCandidates.isEmpty) {
|
if (mergedCandidates.isEmpty) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
SnackBar(
|
||||||
content: Text('Spectrum scan returned no candidate frequencies'),
|
content: Text(AppLocalizations.of(context)!.spectrumScanReturnedNoCandidateFrequencies),
|
||||||
backgroundColor: Colors.red,
|
backgroundColor: Colors.red,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -383,7 +384,7 @@ class _SpectrumScanScreenState extends State<SpectrumScanScreen> {
|
|||||||
: possibleBracketFrequencies;
|
: possibleBracketFrequencies;
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
appBar: AppBar(title: const Text('Spectrum Scan')),
|
appBar: AppBar(title: Text(AppLocalizations.of(context)!.spectrumScan)),
|
||||||
body: ListView(
|
body: ListView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
|
|||||||
@@ -331,7 +331,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
if (_currentPage > 0)
|
if (_currentPage > 0)
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _previousPage,
|
onPressed: _previousPage,
|
||||||
icon: const Icon(Icons.arrow_back),
|
icon: Icon(Icons.arrow_back),
|
||||||
label: Text(l10n.wizardBack),
|
label: Text(l10n.wizardBack),
|
||||||
)
|
)
|
||||||
else
|
else
|
||||||
@@ -510,7 +510,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
? colorScheme.primary
|
? colorScheme.primary
|
||||||
: colorScheme.onSurfaceVariant,
|
: colorScheme.onSurfaceVariant,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
isConnected
|
isConnected
|
||||||
@@ -601,7 +601,7 @@ class _WelcomeWizardScreenState extends State<WelcomeWizardScreen> {
|
|||||||
_selectedPreset.summary,
|
_selectedPreset.summary,
|
||||||
style: Theme.of(context).textTheme.bodyMedium,
|
style: Theme.of(context).textTheme.bodyMedium,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
SizedBox(height: 14),
|
||||||
...[
|
...[
|
||||||
_FeatureItem(
|
_FeatureItem(
|
||||||
icon: Icons.rule,
|
icon: Icons.rule,
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:meshcore_client/meshcore_client.dart';
|
import 'package:meshcore_client/meshcore_client.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
/// Background location tracking service for SAR operations
|
/// Background location tracking service for SAR operations
|
||||||
/// Tracks user location and sends periodic updates via MeshCore BLE
|
/// Tracks user location and sends periodic updates via MeshCore BLE
|
||||||
@@ -15,6 +16,11 @@ class BackgroundLocationService {
|
|||||||
static const String _prefKeyLastLon = 'background_last_lon';
|
static const String _prefKeyLastLon = 'background_last_lon';
|
||||||
|
|
||||||
MeshCoreBleService? _bleService;
|
MeshCoreBleService? _bleService;
|
||||||
|
|
||||||
|
String _scopedKey(String baseKey) {
|
||||||
|
return ProfileStorageScope.scopedKey(baseKey);
|
||||||
|
}
|
||||||
|
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
StreamSubscription<Position>? _positionSubscription;
|
StreamSubscription<Position>? _positionSubscription;
|
||||||
|
|
||||||
@@ -71,8 +77,8 @@ class BackgroundLocationService {
|
|||||||
|
|
||||||
// Save settings
|
// Save settings
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_prefKeyEnabled, true);
|
await prefs.setBool(_scopedKey(_prefKeyEnabled), true);
|
||||||
await prefs.setDouble(_prefKeyDistance, distanceThreshold);
|
await prefs.setDouble(_scopedKey(_prefKeyDistance), distanceThreshold);
|
||||||
|
|
||||||
// Start listening to position updates
|
// Start listening to position updates
|
||||||
Position? lastPosition;
|
Position? lastPosition;
|
||||||
@@ -110,8 +116,14 @@ class BackgroundLocationService {
|
|||||||
lastPosition = position;
|
lastPosition = position;
|
||||||
|
|
||||||
// Save to preferences
|
// Save to preferences
|
||||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
await prefs.setDouble(
|
||||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
_scopedKey(_prefKeyLastLat),
|
||||||
|
position.latitude,
|
||||||
|
);
|
||||||
|
await prefs.setDouble(
|
||||||
|
_scopedKey(_prefKeyLastLon),
|
||||||
|
position.longitude,
|
||||||
|
);
|
||||||
|
|
||||||
// Update device's advertised location
|
// Update device's advertised location
|
||||||
if (_bleService != null && _bleService!.isConnected) {
|
if (_bleService != null && _bleService!.isConnected) {
|
||||||
@@ -161,7 +173,7 @@ class BackgroundLocationService {
|
|||||||
_positionSubscription = null;
|
_positionSubscription = null;
|
||||||
|
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_prefKeyEnabled, false);
|
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||||
debugPrint('✅ [BackgroundLocation] Tracking stopped');
|
debugPrint('✅ [BackgroundLocation] Tracking stopped');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +181,13 @@ class BackgroundLocationService {
|
|||||||
/// Note: This will restart tracking with the new threshold
|
/// Note: This will restart tracking with the new threshold
|
||||||
Future<void> updateDistanceThreshold(double distance) async {
|
Future<void> updateDistanceThreshold(double distance) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setDouble(_prefKeyDistance, distance);
|
await prefs.setDouble(_scopedKey(_prefKeyDistance), distance);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
'📏 [BackgroundLocation] Distance threshold updated to ${distance}m',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Restart tracking if currently enabled
|
// Restart tracking if currently enabled
|
||||||
final isEnabled = prefs.getBool(_prefKeyEnabled) ?? false;
|
final isEnabled = prefs.getBool(_scopedKey(_prefKeyEnabled)) ?? false;
|
||||||
if (isEnabled && _bleService != null) {
|
if (isEnabled && _bleService != null) {
|
||||||
await stopTracking();
|
await stopTracking();
|
||||||
await startTracking(distanceThreshold: distance);
|
await startTracking(distanceThreshold: distance);
|
||||||
|
|||||||
178
lib/services/bthome_met_history.dart
Normal file
178
lib/services/bthome_met_history.dart
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
enum BTHomeMetMeasurement {
|
||||||
|
temperature(1, 'Temperature', '°C'),
|
||||||
|
humidity(2, 'Humidity', '%'),
|
||||||
|
windSpeed(3, 'Wind speed', 'm/s'),
|
||||||
|
gust(4, 'Wind gust', 'm/s'),
|
||||||
|
rain(5, 'Rain', 'mm');
|
||||||
|
|
||||||
|
const BTHomeMetMeasurement(this.id, this.label, this.unit);
|
||||||
|
|
||||||
|
final int id;
|
||||||
|
final String label;
|
||||||
|
final String unit;
|
||||||
|
|
||||||
|
static BTHomeMetMeasurement? fromId(int id) {
|
||||||
|
for (final value in BTHomeMetMeasurement.values) {
|
||||||
|
if (value.id == id) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BTHomeMetHistoryPage {
|
||||||
|
const BTHomeMetHistoryPage({
|
||||||
|
required this.measurement,
|
||||||
|
required this.page,
|
||||||
|
required this.values,
|
||||||
|
});
|
||||||
|
|
||||||
|
final BTHomeMetMeasurement measurement;
|
||||||
|
final int page;
|
||||||
|
final List<double> values;
|
||||||
|
|
||||||
|
double? get latest => values.isEmpty ? null : values.last;
|
||||||
|
double? get minimum => values.isEmpty
|
||||||
|
? null
|
||||||
|
: values.reduce((left, right) => left < right ? left : right);
|
||||||
|
double? get maximum => values.isEmpty
|
||||||
|
? null
|
||||||
|
: values.reduce((left, right) => left > right ? left : right);
|
||||||
|
}
|
||||||
|
|
||||||
|
class BTHomeMetHistoryFormatException implements Exception {
|
||||||
|
const BTHomeMetHistoryFormatException(this.message);
|
||||||
|
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => message;
|
||||||
|
}
|
||||||
|
|
||||||
|
class BTHomeMetHistoryParser {
|
||||||
|
static BTHomeMetHistoryPage parse(String text) {
|
||||||
|
final parts = text
|
||||||
|
.trim()
|
||||||
|
.split(',')
|
||||||
|
.map((part) => part.trim())
|
||||||
|
.toList(growable: false);
|
||||||
|
if (parts.length < 3) {
|
||||||
|
throw const BTHomeMetHistoryFormatException(
|
||||||
|
'MET history response is too short.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final measurementId = int.tryParse(parts[0]);
|
||||||
|
final page = int.tryParse(parts[1]);
|
||||||
|
final count = int.tryParse(parts[2]);
|
||||||
|
if (measurementId == null || page == null || count == null) {
|
||||||
|
throw const BTHomeMetHistoryFormatException(
|
||||||
|
'MET history header is invalid.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final measurement = BTHomeMetMeasurement.fromId(measurementId);
|
||||||
|
if (measurement == null) {
|
||||||
|
throw BTHomeMetHistoryFormatException(
|
||||||
|
'Unsupported MET history measurement id: $measurementId',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (count < 0) {
|
||||||
|
throw const BTHomeMetHistoryFormatException(
|
||||||
|
'MET history sample count is invalid.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (parts.length != count + 3) {
|
||||||
|
throw BTHomeMetHistoryFormatException(
|
||||||
|
'MET history sample count mismatch: expected $count values, got ${parts.length - 3}.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final values = <double>[];
|
||||||
|
for (final part in parts.skip(3)) {
|
||||||
|
final value = double.tryParse(part);
|
||||||
|
if (value == null) {
|
||||||
|
throw BTHomeMetHistoryFormatException(
|
||||||
|
'Invalid MET history sample value: $part',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
values.add(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BTHomeMetHistoryPage(
|
||||||
|
measurement: measurement,
|
||||||
|
page: page,
|
||||||
|
values: List<double>.unmodifiable(values),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTruthyCapabilityValue(Object? value) {
|
||||||
|
if (value is num) {
|
||||||
|
return value > 0;
|
||||||
|
}
|
||||||
|
if (value is bool) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _hasBTHomeMetCapability(Contact? contact) {
|
||||||
|
final extraSensorData = contact?.telemetry?.extraSensorData;
|
||||||
|
if (extraSensorData == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// MET history is only enabled when the node advertises an explicit
|
||||||
|
// capability marker on channel 1. Accept a small set of channel-1 marker
|
||||||
|
// keys so the app remains tolerant while firmware-side encoding settles.
|
||||||
|
const capabilityKeys = <String>[
|
||||||
|
'met_capability',
|
||||||
|
'met_capability_1',
|
||||||
|
'generic_sensor_1',
|
||||||
|
'light_level_1',
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final key in capabilityKeys) {
|
||||||
|
if (_isTruthyCapabilityValue(extraSensorData[key])) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<BTHomeMetMeasurement> bTHomeMetMeasurementsForContact(Contact? contact) {
|
||||||
|
final telemetry = contact?.telemetry;
|
||||||
|
if (telemetry == null || !_hasBTHomeMetCapability(contact)) {
|
||||||
|
return const <BTHomeMetMeasurement>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
final measurements = <BTHomeMetMeasurement>[
|
||||||
|
if (telemetry.temperature != null) BTHomeMetMeasurement.temperature,
|
||||||
|
if (telemetry.humidity != null) BTHomeMetMeasurement.humidity,
|
||||||
|
];
|
||||||
|
|
||||||
|
final extraSensorData = telemetry.extraSensorData;
|
||||||
|
if (extraSensorData != null) {
|
||||||
|
if (extraSensorData.keys.any(
|
||||||
|
(key) => key.startsWith('speed_') || key.startsWith('signed_speed_'),
|
||||||
|
)) {
|
||||||
|
measurements.add(BTHomeMetMeasurement.windSpeed);
|
||||||
|
}
|
||||||
|
if (extraSensorData.keys.any((key) => key.startsWith('gust_'))) {
|
||||||
|
measurements.add(BTHomeMetMeasurement.gust);
|
||||||
|
}
|
||||||
|
if (extraSensorData.keys.any((key) => key.startsWith('rain_'))) {
|
||||||
|
measurements.add(BTHomeMetMeasurement.rain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return List<BTHomeMetMeasurement>.unmodifiable(measurements);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool supportsBTHomeMetHistory(Contact? contact) =>
|
||||||
|
bTHomeMetMeasurementsForContact(contact).isNotEmpty;
|
||||||
@@ -5,6 +5,76 @@ import 'package:meshcore_client/meshcore_client.dart';
|
|||||||
/// Cayenne LPP (Low Power Payload) data parser
|
/// Cayenne LPP (Low Power Payload) data parser
|
||||||
/// Used for decoding telemetry sensor data from MeshCore devices
|
/// Used for decoding telemetry sensor data from MeshCore devices
|
||||||
class CayenneLppParser {
|
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
|
/// Parse Cayenne LPP data into ContactTelemetry
|
||||||
static ContactTelemetry parse(Uint8List data) {
|
static ContactTelemetry parse(Uint8List data) {
|
||||||
debugPrint(' [CayenneLPP] Parsing LPP data...');
|
debugPrint(' [CayenneLPP] Parsing LPP data...');
|
||||||
@@ -18,14 +88,18 @@ class CayenneLppParser {
|
|||||||
LatLng? gpsLocation;
|
LatLng? gpsLocation;
|
||||||
double? batteryPercentage;
|
double? batteryPercentage;
|
||||||
double? batteryMilliVolts;
|
double? batteryMilliVolts;
|
||||||
|
double? deferredBatteryMilliVolts;
|
||||||
|
double? deferredBatteryPercentage;
|
||||||
double? temperature;
|
double? temperature;
|
||||||
double? humidity;
|
double? humidity;
|
||||||
double? pressure;
|
double? pressure;
|
||||||
final extraSensorData = <String, dynamic>{};
|
final extraSensorData = <String, dynamic>{};
|
||||||
|
bool sawNonSelfChannel = false;
|
||||||
|
|
||||||
int fieldCount = 0;
|
int fieldCount = 0;
|
||||||
while (reader.hasRemaining) {
|
while (reader.hasRemaining) {
|
||||||
if (fieldCount > 0 && _isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
if (fieldCount > 0 &&
|
||||||
|
_isZeroPaddedTail(data, reader.remainingBytesCount)) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' Detected zero-padded telemetry tail, stopping parse at position '
|
' Detected zero-padded telemetry tail, stopping parse at position '
|
||||||
'${data.length - reader.remainingBytesCount}',
|
'${data.length - reader.remainingBytesCount}',
|
||||||
@@ -41,6 +115,9 @@ class CayenneLppParser {
|
|||||||
|
|
||||||
final channel = reader.readByte();
|
final channel = reader.readByte();
|
||||||
debugPrint(' Channel: $channel');
|
debugPrint(' Channel: $channel');
|
||||||
|
if (channel != _selfTelemetryChannel) {
|
||||||
|
sawNonSelfChannel = true;
|
||||||
|
}
|
||||||
|
|
||||||
final type = reader.readByte();
|
final type = reader.readByte();
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -65,14 +142,20 @@ class CayenneLppParser {
|
|||||||
final value = rawValue / 100.0;
|
final value = rawValue / 100.0;
|
||||||
debugPrint(' Analog Input (raw): $rawValue');
|
debugPrint(' Analog Input (raw): $rawValue');
|
||||||
debugPrint(' Analog Input (volts): ${value}V');
|
debugPrint(' Analog Input (volts): ${value}V');
|
||||||
extraSensorData['analog_input_$channel'] = value;
|
if (_isDedicatedBatteryChannel(channel)) {
|
||||||
// If this is a battery reading
|
|
||||||
if (channel == 0 || channel == 1) {
|
|
||||||
batteryMilliVolts = value * 1000;
|
batteryMilliVolts = value * 1000;
|
||||||
batteryPercentage = _calculateBatteryPercentage(value);
|
batteryPercentage = _calculateBatteryPercentage(value);
|
||||||
|
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||||
|
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||||
);
|
);
|
||||||
|
} else if (_isDeferredBatteryChannel(channel)) {
|
||||||
|
deferredBatteryMilliVolts = value * 1000;
|
||||||
|
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||||
|
extraSensorData['analog_input_$channel'] = value;
|
||||||
|
} else {
|
||||||
|
extraSensorData['analog_input_$channel'] = value;
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -85,7 +168,7 @@ class CayenneLppParser {
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case MeshCoreConstants.lppIlluminanceSensor:
|
case MeshCoreConstants.lppIlluminanceSensor:
|
||||||
final value = reader.readUInt16BE();
|
final value = reader.readUInt16BE().toDouble();
|
||||||
debugPrint(' Illuminance: $value lux');
|
debugPrint(' Illuminance: $value lux');
|
||||||
extraSensorData['illuminance_$channel'] = value;
|
extraSensorData['illuminance_$channel'] = value;
|
||||||
break;
|
break;
|
||||||
@@ -98,18 +181,36 @@ class CayenneLppParser {
|
|||||||
|
|
||||||
case MeshCoreConstants.lppTemperatureSensor:
|
case MeshCoreConstants.lppTemperatureSensor:
|
||||||
final rawValue = reader.readInt16BE();
|
final rawValue = reader.readInt16BE();
|
||||||
temperature = rawValue / 10.0;
|
final value = rawValue / 10.0;
|
||||||
debugPrint(' Temperature (raw): $rawValue');
|
debugPrint(' Temperature (raw): $rawValue');
|
||||||
debugPrint(
|
debugPrint(' Temperature: ${value.toStringAsFixed(1)}°C');
|
||||||
' Temperature: ${temperature.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;
|
break;
|
||||||
|
|
||||||
case MeshCoreConstants.lppHumiditySensor:
|
case MeshCoreConstants.lppHumiditySensor:
|
||||||
final rawValue = reader.readByte();
|
final rawValue = reader.readByte();
|
||||||
humidity = rawValue / 2.0;
|
final value = rawValue / 2.0;
|
||||||
debugPrint(' Humidity (raw): $rawValue');
|
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;
|
break;
|
||||||
|
|
||||||
case MeshCoreConstants.lppAccelerometer:
|
case MeshCoreConstants.lppAccelerometer:
|
||||||
@@ -126,9 +227,19 @@ class CayenneLppParser {
|
|||||||
|
|
||||||
case MeshCoreConstants.lppBarometer:
|
case MeshCoreConstants.lppBarometer:
|
||||||
final rawValue = reader.readUInt16BE();
|
final rawValue = reader.readUInt16BE();
|
||||||
pressure = rawValue / 10.0;
|
final value = rawValue / 10.0;
|
||||||
debugPrint(' Barometer (raw): $rawValue');
|
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;
|
break;
|
||||||
|
|
||||||
case MeshCoreConstants.lppVoltageSensor:
|
case MeshCoreConstants.lppVoltageSensor:
|
||||||
@@ -136,12 +247,21 @@ class CayenneLppParser {
|
|||||||
final value = rawValue / 100.0;
|
final value = rawValue / 100.0;
|
||||||
debugPrint(' Voltage (raw): $rawValue');
|
debugPrint(' Voltage (raw): $rawValue');
|
||||||
debugPrint(' Voltage: ${value}V');
|
debugPrint(' Voltage: ${value}V');
|
||||||
// Treat voltage sensor as battery reading
|
if (_isDedicatedBatteryChannel(channel)) {
|
||||||
batteryMilliVolts = value * 1000;
|
batteryMilliVolts = value * 1000;
|
||||||
batteryPercentage = _calculateBatteryPercentage(value);
|
batteryPercentage = _calculateBatteryPercentage(value);
|
||||||
debugPrint(
|
extraSensorData[_sourceChannelKey('battery')] = channel;
|
||||||
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
extraSensorData[_sourceChannelKey('voltage')] = channel;
|
||||||
);
|
debugPrint(
|
||||||
|
' → Battery: ${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||||
|
);
|
||||||
|
} else if (_isDeferredBatteryChannel(channel)) {
|
||||||
|
deferredBatteryMilliVolts = value * 1000;
|
||||||
|
deferredBatteryPercentage = _calculateBatteryPercentage(value);
|
||||||
|
extraSensorData['voltage_$channel'] = value;
|
||||||
|
} else {
|
||||||
|
extraSensorData['voltage_$channel'] = value;
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case MeshCoreConstants.lppGyrometer:
|
case MeshCoreConstants.lppGyrometer:
|
||||||
@@ -194,15 +314,328 @@ class CayenneLppParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gpsLocation = LatLng(lat, lon);
|
gpsLocation = LatLng(lat, lon);
|
||||||
|
extraSensorData[_sourceChannelKey('gps')] = channel;
|
||||||
extraSensorData['altitude_$channel'] = alt;
|
extraSensorData['altitude_$channel'] = alt;
|
||||||
break;
|
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 (_isDedicatedBatteryChannel(channel) || _isDeferredBatteryChannel(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:
|
default:
|
||||||
debugPrint(
|
final size = _payloadSizeForType(type);
|
||||||
' ⚠️ Unknown type, skipping remaining ${reader.remainingBytesCount} bytes',
|
if (size == null || reader.remainingBytesCount < size) {
|
||||||
);
|
debugPrint(
|
||||||
// Unknown type, skip remaining to avoid parsing errors
|
' ⚠️ Unknown type $type with unsupported size, stopping parse',
|
||||||
reader.skip(reader.remainingBytesCount);
|
);
|
||||||
|
reader.skip(reader.remainingBytesCount);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
debugPrint(' ⚠️ Unknown type $type, skipping $size bytes');
|
||||||
|
reader.skip(size);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -212,6 +645,21 @@ class CayenneLppParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (batteryMilliVolts == null &&
|
||||||
|
batteryPercentage == null &&
|
||||||
|
deferredBatteryMilliVolts != null &&
|
||||||
|
deferredBatteryPercentage != null &&
|
||||||
|
!sawNonSelfChannel) {
|
||||||
|
batteryMilliVolts = deferredBatteryMilliVolts;
|
||||||
|
batteryPercentage = deferredBatteryPercentage;
|
||||||
|
extraSensorData[_sourceChannelKey('battery')] = _selfTelemetryChannel;
|
||||||
|
extraSensorData[_sourceChannelKey('voltage')] = _selfTelemetryChannel;
|
||||||
|
debugPrint(
|
||||||
|
' Promoted self-channel voltage to battery: '
|
||||||
|
'${batteryPercentage.toStringAsFixed(1)}% (${batteryMilliVolts.toStringAsFixed(0)}mV)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
debugPrint(' Parsed $fieldCount fields');
|
debugPrint(' Parsed $fieldCount fields');
|
||||||
debugPrint(' ✅ [CayenneLPP] Parsing complete');
|
debugPrint(' ✅ [CayenneLPP] Parsing complete');
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -258,6 +706,165 @@ class CayenneLppParser {
|
|||||||
return ((voltage - 3.0) / 1.2) * 100.0;
|
return ((voltage - 3.0) / 1.2) * 100.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool _isDedicatedBatteryChannel(int channel) => channel == 0;
|
||||||
|
|
||||||
|
static bool _isDeferredBatteryChannel(int channel) =>
|
||||||
|
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) {
|
static bool _isZeroPaddedTail(Uint8List data, int remainingBytes) {
|
||||||
final start = data.length - remainingBytes;
|
final start = data.length - remainingBytes;
|
||||||
for (int i = start; i < data.length; i++) {
|
for (int i = start; i < data.length; i++) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/contact_group.dart';
|
import '../models/contact_group.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
import '../utils/key_comparison.dart';
|
import '../utils/key_comparison.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
@@ -10,10 +11,16 @@ import 'package:latlong2/latlong.dart';
|
|||||||
class ContactStorageService {
|
class ContactStorageService {
|
||||||
static const String _contactsKey = 'stored_contacts';
|
static const String _contactsKey = 'stored_contacts';
|
||||||
static const String _contactGroupsKey = 'stored_contact_groups';
|
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 _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
|
/// Save contacts to persistent storage
|
||||||
Future<void> saveContacts(List<Contact> contacts) async {
|
Future<void> saveContacts(List<Contact> contacts, {String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
@@ -28,7 +35,10 @@ class ContactStorageService {
|
|||||||
: jsonList;
|
: jsonList;
|
||||||
|
|
||||||
final jsonString = jsonEncode(limitedList);
|
final jsonString = jsonEncode(limitedList);
|
||||||
await prefs.setString(_contactsKey, jsonString);
|
await prefs.setString(
|
||||||
|
_key(_contactsKey, namespace: namespace),
|
||||||
|
jsonString,
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
'✅ [ContactStorage] Saved ${limitedList.length} contacts to storage',
|
||||||
@@ -40,10 +50,15 @@ class ContactStorageService {
|
|||||||
|
|
||||||
/// Load contacts from persistent storage
|
/// Load contacts from persistent storage
|
||||||
/// [excludePublicKey] - optional public key to exclude (e.g., device's own key)
|
/// [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 {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_contactsKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_contactsKey, namespace: namespace),
|
||||||
|
);
|
||||||
|
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
debugPrint('ℹ️ [ContactStorage] No stored contacts found');
|
debugPrint('ℹ️ [ContactStorage] No stored contacts found');
|
||||||
@@ -82,23 +97,29 @@ class ContactStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all stored contacts
|
/// Clear all stored contacts
|
||||||
Future<void> clearContacts() async {
|
Future<void> clearContacts({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_contactsKey);
|
await prefs.remove(_key(_contactsKey, namespace: namespace));
|
||||||
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
|
debugPrint('✅ [ContactStorage] Cleared all stored contacts');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
|
debugPrint('❌ [ContactStorage] Error clearing contacts: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveContactGroups(List<SavedContactGroup> groups) async {
|
Future<void> saveContactGroups(
|
||||||
|
List<SavedContactGroup> groups, {
|
||||||
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = jsonEncode(
|
final jsonString = jsonEncode(
|
||||||
groups.map((group) => _contactGroupToJson(group)).toList(),
|
groups.map((group) => _contactGroupToJson(group)).toList(),
|
||||||
);
|
);
|
||||||
await prefs.setString(_contactGroupsKey, jsonString);
|
await prefs.setString(
|
||||||
|
_key(_contactGroupsKey, namespace: namespace),
|
||||||
|
jsonString,
|
||||||
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
|
'✅ [ContactStorage] Saved ${groups.length} contact groups to storage',
|
||||||
);
|
);
|
||||||
@@ -107,10 +128,12 @@ class ContactStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<SavedContactGroup>> loadContactGroups() async {
|
Future<List<SavedContactGroup>> loadContactGroups({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_contactGroupsKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_contactGroupsKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -126,11 +149,106 @@ class ContactStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get storage statistics
|
Future<void> clearContactGroups({String? namespace}) async {
|
||||||
Future<Map<String, dynamic>> getStorageStats() async {
|
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_contactsKey);
|
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) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
return {'contactCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||||
@@ -167,6 +285,15 @@ class ContactStorageService {
|
|||||||
'telemetry': contact.telemetry != null
|
'telemetry': contact.telemetry != null
|
||||||
? _telemetryToJson(contact.telemetry!)
|
? _telemetryToJson(contact.telemetry!)
|
||||||
: null,
|
: null,
|
||||||
|
'advertHistory': contact.advertHistory
|
||||||
|
.map(
|
||||||
|
(point) => {
|
||||||
|
'lat': point.location.latitude,
|
||||||
|
'lon': point.location.longitude,
|
||||||
|
'tsMillis': point.timestamp.millisecondsSinceEpoch,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.toList(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -190,6 +317,7 @@ class ContactStorageService {
|
|||||||
telemetry: json['telemetry'] != null
|
telemetry: json['telemetry'] != null
|
||||||
? _telemetryFromJson(json['telemetry'] as Map<String, dynamic>)
|
? _telemetryFromJson(json['telemetry'] as Map<String, dynamic>)
|
||||||
: null,
|
: null,
|
||||||
|
advertHistory: _advertHistoryFromJson(json['advertHistory']),
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ContactStorage] Error parsing contact from JSON: $e');
|
debugPrint('❌ [ContactStorage] Error parsing contact from JSON: $e');
|
||||||
@@ -242,6 +370,25 @@ class ContactStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
Map<String, dynamic> _contactGroupToJson(SavedContactGroup group) {
|
||||||
return {
|
return {
|
||||||
'id': group.id,
|
'id': group.id,
|
||||||
|
|||||||
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 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
/// Stores user-selected image compression settings.
|
/// Stores user-selected image compression settings.
|
||||||
class ImagePreferences {
|
class ImagePreferences {
|
||||||
@@ -17,44 +18,53 @@ class ImagePreferences {
|
|||||||
|
|
||||||
static Future<int> getMaxSize() async {
|
static Future<int> getMaxSize() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
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;
|
return supportedSizes.contains(value) ? value : defaultMaxSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setMaxSize(int size) async {
|
static Future<void> setMaxSize(int size) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setInt(_maxSizeKey, size);
|
await prefs.setInt(ProfileStorageScope.scopedKey(_maxSizeKey), size);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<int> getCompression() async {
|
static Future<int> getCompression() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final value = prefs.getInt(_qualityKey) ?? defaultQuality;
|
final value =
|
||||||
|
prefs.getInt(ProfileStorageScope.scopedKey(_qualityKey)) ??
|
||||||
|
defaultQuality;
|
||||||
return value.clamp(10, 90);
|
return value.clamp(10, 90);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setCompression(int compression) async {
|
static Future<void> setCompression(int compression) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
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 {
|
static Future<bool> getGrayscale() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool(_grayscaleKey) ?? defaultGrayscale;
|
return prefs.getBool(ProfileStorageScope.scopedKey(_grayscaleKey)) ??
|
||||||
|
defaultGrayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setGrayscale(bool value) async {
|
static Future<void> setGrayscale(bool value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_grayscaleKey, value);
|
await prefs.setBool(ProfileStorageScope.scopedKey(_grayscaleKey), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> getUltraMode() async {
|
static Future<bool> getUltraMode() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool(_ultraModeKey) ?? defaultUltraMode;
|
return prefs.getBool(ProfileStorageScope.scopedKey(_ultraModeKey)) ??
|
||||||
|
defaultUltraMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setUltraMode(bool value) async {
|
static Future<void> setUltraMode(bool value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_ultraModeKey, value);
|
await prefs.setBool(ProfileStorageScope.scopedKey(_ultraModeKey), value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int effectiveMaxSize(
|
static int effectiveMaxSize(
|
||||||
|
|||||||
@@ -141,6 +141,18 @@ class LiveTrafficSummary {
|
|||||||
|
|
||||||
const LiveTrafficSummary._();
|
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(
|
static LiveTrafficSnapshot fromLogs(
|
||||||
Iterable<BlePacketLog> logs, {
|
Iterable<BlePacketLog> logs, {
|
||||||
required DateTime now,
|
required DateTime now,
|
||||||
@@ -154,15 +166,14 @@ class LiveTrafficSummary {
|
|||||||
? clearedAt
|
? clearedAt
|
||||||
: windowStart;
|
: windowStart;
|
||||||
|
|
||||||
final recentLogs = logs
|
final recentLogs =
|
||||||
.where(
|
logs
|
||||||
(log) =>
|
.where(
|
||||||
log.direction == PacketDirection.rx &&
|
(log) =>
|
||||||
log.responseCode == logRxDataResponseCode &&
|
isRxDataLog(log) && !log.timestamp.isBefore(effectiveStart),
|
||||||
!log.timestamp.isBefore(effectiveStart),
|
)
|
||||||
)
|
.toList()
|
||||||
.toList()
|
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
|
||||||
|
|
||||||
final entries = <LiveTrafficEntry>[];
|
final entries = <LiveTrafficEntry>[];
|
||||||
for (final log in recentLogs) {
|
for (final log in recentLogs) {
|
||||||
@@ -215,7 +226,9 @@ class LiveTrafficSummary {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final visibleEntries = filteredEntries.reversed.take(maxVisibleEntries).toList();
|
final visibleEntries = filteredEntries.reversed
|
||||||
|
.take(maxVisibleEntries)
|
||||||
|
.toList();
|
||||||
const txCount = 0;
|
const txCount = 0;
|
||||||
final totalCount = rxCount;
|
final totalCount = rxCount;
|
||||||
final packetsPerMinute =
|
final packetsPerMinute =
|
||||||
@@ -233,7 +246,9 @@ class LiveTrafficSummary {
|
|||||||
avgRssiDbm: rssiCount == 0 ? null : rssiSum / rssiCount,
|
avgRssiDbm: rssiCount == 0 ? null : rssiSum / rssiCount,
|
||||||
latestRssiDbm: latestRssiDbm,
|
latestRssiDbm: latestRssiDbm,
|
||||||
multiHopCount: multiHopCount,
|
multiHopCount: multiHopCount,
|
||||||
avgHopCount: hopCountSamples == 0 ? null : hopCountTotal / hopCountSamples,
|
avgHopCount: hopCountSamples == 0
|
||||||
|
? null
|
||||||
|
: hopCountTotal / hopCountSamples,
|
||||||
visibleEntries: visibleEntries,
|
visibleEntries: visibleEntries,
|
||||||
busyness: _busynessForPacketsPerMinute(packetsPerMinute),
|
busyness: _busynessForPacketsPerMinute(packetsPerMinute),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
|||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:meshcore_client/meshcore_client.dart';
|
import 'package:meshcore_client/meshcore_client.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
/// Centralized location tracking service for MeshCore SAR
|
/// Centralized location tracking service for MeshCore SAR
|
||||||
///
|
///
|
||||||
@@ -49,6 +50,10 @@ class LocationTrackingService {
|
|||||||
static const String _prefKeyFastActiveCadence =
|
static const String _prefKeyFastActiveCadence =
|
||||||
'fast_location_active_cadence_seconds';
|
'fast_location_active_cadence_seconds';
|
||||||
|
|
||||||
|
String _scopedKey(String baseKey) {
|
||||||
|
return ProfileStorageScope.scopedKey(baseKey);
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Configuration Properties
|
// Configuration Properties
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -399,7 +404,7 @@ class LocationTrackingService {
|
|||||||
|
|
||||||
// Save disabled state
|
// Save disabled state
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_prefKeyEnabled, false);
|
await prefs.setBool(_scopedKey(_prefKeyEnabled), false);
|
||||||
|
|
||||||
debugPrint('✅ [LocationTracking] Tracking stopped');
|
debugPrint('✅ [LocationTracking] Tracking stopped');
|
||||||
}
|
}
|
||||||
@@ -477,8 +482,8 @@ class LocationTrackingService {
|
|||||||
|
|
||||||
// Save to preferences
|
// Save to preferences
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setDouble(_prefKeyLastLat, position.latitude);
|
await prefs.setDouble(_scopedKey(_prefKeyLastLat), position.latitude);
|
||||||
await prefs.setDouble(_prefKeyLastLon, position.longitude);
|
await prefs.setDouble(_scopedKey(_prefKeyLastLon), position.longitude);
|
||||||
|
|
||||||
debugPrint('✅ [LocationTracking] Initial position set without broadcast');
|
debugPrint('✅ [LocationTracking] Initial position set without broadcast');
|
||||||
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
|
debugPrint(' Next broadcast allowed in ${minTimeIntervalSeconds}s');
|
||||||
@@ -646,17 +651,24 @@ class LocationTrackingService {
|
|||||||
Future<void> loadSettings() async {
|
Future<void> loadSettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
minDistanceMeters = prefs.getDouble(_prefKeyMinDistance) ?? 5.0;
|
minDistanceMeters = prefs.getDouble(_scopedKey(_prefKeyMinDistance)) ?? 5.0;
|
||||||
maxDistanceMeters = prefs.getDouble(_prefKeyMaxDistance) ?? 100.0;
|
maxDistanceMeters =
|
||||||
minTimeIntervalSeconds = prefs.getInt(_prefKeyMinTimeInterval) ?? 30;
|
prefs.getDouble(_scopedKey(_prefKeyMaxDistance)) ?? 100.0;
|
||||||
gpsUpdateDistance = prefs.getDouble(_prefKeyGpsUpdateDistance) ?? 10.0;
|
minTimeIntervalSeconds =
|
||||||
|
prefs.getInt(_scopedKey(_prefKeyMinTimeInterval)) ?? 30;
|
||||||
|
gpsUpdateDistance =
|
||||||
|
prefs.getDouble(_scopedKey(_prefKeyGpsUpdateDistance)) ?? 10.0;
|
||||||
fastLocationUpdatesEnabled =
|
fastLocationUpdatesEnabled =
|
||||||
prefs.getBool(_prefKeyFastLocationEnabled) ?? false;
|
prefs.getBool(_scopedKey(_prefKeyFastLocationEnabled)) ?? false;
|
||||||
fastLocationMovementThresholdMeters =
|
fastLocationMovementThresholdMeters =
|
||||||
(prefs.getDouble(_prefKeyFastMovementThreshold) ?? gpsUpdateDistance)
|
(prefs.getDouble(_scopedKey(_prefKeyFastMovementThreshold)) ??
|
||||||
|
gpsUpdateDistance)
|
||||||
.clamp(1.0, 1000.0);
|
.clamp(1.0, 1000.0);
|
||||||
fastLocationActiveCadenceSeconds =
|
fastLocationActiveCadenceSeconds =
|
||||||
(prefs.getInt(_prefKeyFastActiveCadence) ?? 10).clamp(5, 60);
|
(prefs.getInt(_scopedKey(_prefKeyFastActiveCadence)) ?? 10).clamp(
|
||||||
|
5,
|
||||||
|
60,
|
||||||
|
);
|
||||||
|
|
||||||
debugPrint('✅ [LocationTracking] Settings loaded');
|
debugPrint('✅ [LocationTracking] Settings loaded');
|
||||||
debugPrint(' Min distance: ${minDistanceMeters}m');
|
debugPrint(' Min distance: ${minDistanceMeters}m');
|
||||||
@@ -674,21 +686,27 @@ class LocationTrackingService {
|
|||||||
Future<void> saveSettings() async {
|
Future<void> saveSettings() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
|
||||||
await prefs.setDouble(_prefKeyMinDistance, minDistanceMeters);
|
await prefs.setDouble(_scopedKey(_prefKeyMinDistance), minDistanceMeters);
|
||||||
await prefs.setDouble(_prefKeyMaxDistance, maxDistanceMeters);
|
await prefs.setDouble(_scopedKey(_prefKeyMaxDistance), maxDistanceMeters);
|
||||||
await prefs.setInt(_prefKeyMinTimeInterval, minTimeIntervalSeconds);
|
await prefs.setInt(
|
||||||
await prefs.setDouble(_prefKeyGpsUpdateDistance, gpsUpdateDistance);
|
_scopedKey(_prefKeyMinTimeInterval),
|
||||||
await prefs.setBool(_prefKeyEnabled, isTracking);
|
minTimeIntervalSeconds,
|
||||||
|
);
|
||||||
|
await prefs.setDouble(
|
||||||
|
_scopedKey(_prefKeyGpsUpdateDistance),
|
||||||
|
gpsUpdateDistance,
|
||||||
|
);
|
||||||
|
await prefs.setBool(_scopedKey(_prefKeyEnabled), isTracking);
|
||||||
await prefs.setBool(
|
await prefs.setBool(
|
||||||
_prefKeyFastLocationEnabled,
|
_scopedKey(_prefKeyFastLocationEnabled),
|
||||||
fastLocationUpdatesEnabled,
|
fastLocationUpdatesEnabled,
|
||||||
);
|
);
|
||||||
await prefs.setDouble(
|
await prefs.setDouble(
|
||||||
_prefKeyFastMovementThreshold,
|
_scopedKey(_prefKeyFastMovementThreshold),
|
||||||
fastLocationMovementThresholdMeters,
|
fastLocationMovementThresholdMeters,
|
||||||
);
|
);
|
||||||
await prefs.setInt(
|
await prefs.setInt(
|
||||||
_prefKeyFastActiveCadence,
|
_scopedKey(_prefKeyFastActiveCadence),
|
||||||
fastLocationActiveCadenceSeconds,
|
fastLocationActiveCadenceSeconds,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -41,90 +41,103 @@ class MapMarkerService {
|
|||||||
Function(Contact)? onTap,
|
Function(Contact)? onTap,
|
||||||
double mapRotation = 0,
|
double mapRotation = 0,
|
||||||
Position? userPosition,
|
Position? userPosition,
|
||||||
|
Map<String, LatLng>? estimatedLocations,
|
||||||
}) {
|
}) {
|
||||||
return contacts.map((contact) {
|
return contacts
|
||||||
final location = contact.displayLocation;
|
.map((contact) {
|
||||||
if (location == null) return null;
|
final location = contact.displayLocation ??
|
||||||
|
estimatedLocations?[contact.publicKeyHex];
|
||||||
|
if (location == null) return null;
|
||||||
|
|
||||||
return Marker(
|
return Marker(
|
||||||
point: location,
|
point: location,
|
||||||
width: 80,
|
width: 80,
|
||||||
height: 100,
|
height: 100,
|
||||||
rotate: false, // Don't rotate the entire marker with map
|
rotate: false, // Don't rotate the entire marker with map
|
||||||
child: Transform.rotate(
|
child: Transform.rotate(
|
||||||
angle: -mapRotation * pi / 180,
|
angle: -mapRotation * pi / 180,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onTap: onTap != null ? () => onTap(contact) : null,
|
onTap: onTap != null ? () => onTap(contact) : null,
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
// Location update time indicator
|
// Location update time indicator
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: 4,
|
||||||
color: getLocationAgeColor(contact),
|
vertical: 1,
|
||||||
borderRadius: BorderRadius.circular(3),
|
),
|
||||||
),
|
decoration: BoxDecoration(
|
||||||
child: Text(
|
color: getLocationAgeColor(contact),
|
||||||
contact.timeSinceLocationUpdate,
|
borderRadius: BorderRadius.circular(3),
|
||||||
style: const TextStyle(
|
),
|
||||||
color: Colors.white,
|
child: Text(
|
||||||
fontSize: 9,
|
contact.timeSinceLocationUpdate,
|
||||||
fontWeight: FontWeight.bold,
|
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),
|
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
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,
|
const SizedBox(height: 2),
|
||||||
maxLines: 1,
|
// Marker icon
|
||||||
textAlign: TextAlign.center,
|
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>()
|
||||||
}).whereType<Marker>().toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Generate markers for SAR events.
|
/// Generate markers for SAR events.
|
||||||
@@ -141,10 +154,11 @@ class MapMarkerService {
|
|||||||
required BuildContext context,
|
required BuildContext context,
|
||||||
Function(SarMarker)? onTap,
|
Function(SarMarker)? onTap,
|
||||||
double mapRotation = 0,
|
double mapRotation = 0,
|
||||||
|
LatLng Function(LatLng point)? pointTransformer,
|
||||||
}) {
|
}) {
|
||||||
return sarMarkers.map((marker) {
|
return sarMarkers.map((marker) {
|
||||||
return Marker(
|
return Marker(
|
||||||
point: marker.location,
|
point: pointTransformer?.call(marker.location) ?? marker.location,
|
||||||
width: 90,
|
width: 90,
|
||||||
height: 100,
|
height: 100,
|
||||||
rotate: false, // Don't rotate the entire marker with map
|
rotate: false, // Don't rotate the entire marker with map
|
||||||
@@ -157,7 +171,10 @@ class MapMarkerService {
|
|||||||
children: [
|
children: [
|
||||||
// Time ago label
|
// Time ago label
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: getSarMarkerColor(marker.type),
|
color: getSarMarkerColor(marker.type),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
@@ -188,7 +205,7 @@ class MapMarkerService {
|
|||||||
),
|
),
|
||||||
padding: const EdgeInsets.all(6),
|
padding: const EdgeInsets.all(6),
|
||||||
child: Text(
|
child: Text(
|
||||||
marker.emoji, // Use custom emoji if available
|
marker.emoji, // Use custom emoji if available
|
||||||
style: const TextStyle(fontSize: 18),
|
style: const TextStyle(fontSize: 18),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -196,13 +213,17 @@ class MapMarkerService {
|
|||||||
// Type label
|
// Type label
|
||||||
Container(
|
Container(
|
||||||
constraints: const BoxConstraints(maxWidth: 90),
|
constraints: const BoxConstraints(maxWidth: 90),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 1),
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 4,
|
||||||
|
vertical: 1,
|
||||||
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.7),
|
color: Colors.black.withValues(alpha: 0.7),
|
||||||
borderRadius: BorderRadius.circular(3),
|
borderRadius: BorderRadius.circular(3),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
marker.displayName, // Uses notes if available, otherwise type.displayName
|
marker
|
||||||
|
.displayName, // Uses notes if available, otherwise type.displayName
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 9,
|
fontSize: 9,
|
||||||
@@ -269,7 +290,8 @@ class MapMarkerService {
|
|||||||
final dLat = (lat2 - lat1) * pi / 180;
|
final dLat = (lat2 - lat1) * pi / 180;
|
||||||
final dLon = (lon2 - lon1) * 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(lat1 * pi / 180) *
|
||||||
cos(lat2 * pi / 180) *
|
cos(lat2 * pi / 180) *
|
||||||
sin(dLon / 2) *
|
sin(dLon / 2) *
|
||||||
@@ -299,8 +321,8 @@ class MapMarkerService {
|
|||||||
final lat2Rad = lat2 * pi / 180;
|
final lat2Rad = lat2 * pi / 180;
|
||||||
|
|
||||||
final y = sin(dLon) * cos(lat2Rad);
|
final y = sin(dLon) * cos(lat2Rad);
|
||||||
final x = cos(lat1Rad) * sin(lat2Rad) -
|
final x =
|
||||||
sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
cos(lat1Rad) * sin(lat2Rad) - sin(lat1Rad) * cos(lat2Rad) * cos(dLon);
|
||||||
|
|
||||||
final bearing = atan2(y, x) * 180 / pi;
|
final bearing = atan2(y, x) * 180 / pi;
|
||||||
return (bearing + 360) % 360;
|
return (bearing + 360) % 360;
|
||||||
@@ -368,6 +390,8 @@ class MapMarkerService {
|
|||||||
return Colors.deepPurple; // Purple for repeaters
|
return Colors.deepPurple; // Purple for repeaters
|
||||||
case ContactType.room:
|
case ContactType.room:
|
||||||
return Colors.teal; // Teal for rooms
|
return Colors.teal; // Teal for rooms
|
||||||
|
case ContactType.sensor:
|
||||||
|
return Colors.green; // Green for sensors
|
||||||
case ContactType.channel:
|
case ContactType.channel:
|
||||||
return Colors.orange; // Orange for channels
|
return Colors.orange; // Orange for channels
|
||||||
case ContactType.none:
|
case ContactType.none:
|
||||||
@@ -389,6 +413,8 @@ class MapMarkerService {
|
|||||||
return Icons.router; // Router icon for repeaters
|
return Icons.router; // Router icon for repeaters
|
||||||
case ContactType.room:
|
case ContactType.room:
|
||||||
return Icons.forum; // Forum/chat icon for rooms
|
return Icons.forum; // Forum/chat icon for rooms
|
||||||
|
case ContactType.sensor:
|
||||||
|
return Icons.sensors; // Sensors icon for sensor nodes
|
||||||
case ContactType.channel:
|
case ContactType.channel:
|
||||||
return Icons.public; // Public icon for channels
|
return Icons.public; // Public icon for channels
|
||||||
case ContactType.none:
|
case ContactType.none:
|
||||||
@@ -466,7 +492,8 @@ class MapMarkerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (allPoints.isEmpty) {
|
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;
|
double lat = 0, lng = 0;
|
||||||
|
|||||||
99
lib/services/map_workspace_snapshot_service.dart
Normal file
99
lib/services/map_workspace_snapshot_service.dart
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
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,
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,6 +20,14 @@ class MeshMapNode {
|
|||||||
required this.updatedAtMs,
|
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) {
|
factory MeshMapNode.fromJson(Map<String, dynamic> json) {
|
||||||
return MeshMapNode(
|
return MeshMapNode(
|
||||||
type: (json['type'] as num?)?.toInt() ?? 0,
|
type: (json['type'] as num?)?.toInt() ?? 0,
|
||||||
@@ -77,9 +85,7 @@ class MeshMapNodesService {
|
|||||||
final nodes = nodesRaw
|
final nodes = nodesRaw
|
||||||
.whereType<Map<String, dynamic>>()
|
.whereType<Map<String, dynamic>>()
|
||||||
.map(MeshMapNode.fromJson)
|
.map(MeshMapNode.fromJson)
|
||||||
.where(
|
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
|
||||||
)
|
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
await _storeCache(nodes, cachedAt: now);
|
await _storeCache(nodes, cachedAt: now);
|
||||||
@@ -116,9 +122,7 @@ class MeshMapNodesService {
|
|||||||
final nodes = decoded
|
final nodes = decoded
|
||||||
.whereType<Map<String, dynamic>>()
|
.whereType<Map<String, dynamic>>()
|
||||||
.map(MeshMapNode.fromJson)
|
.map(MeshMapNode.fromJson)
|
||||||
.where(
|
.where((n) => n.publicKey.isNotEmpty && n.hasValidCoordinates)
|
||||||
(n) => n.publicKey.isNotEmpty && n.latitude != 0 && n.longitude != 0,
|
|
||||||
)
|
|
||||||
.toList();
|
.toList();
|
||||||
_cachedNodes = nodes;
|
_cachedNodes = nodes;
|
||||||
_cachedAt = cachedAt;
|
_cachedAt = cachedAt;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../models/message_contact_location.dart';
|
|||||||
import '../models/message_reception_details.dart';
|
import '../models/message_reception_details.dart';
|
||||||
import '../models/message_transfer_details.dart';
|
import '../models/message_transfer_details.dart';
|
||||||
import '../models/message_route_metadata.dart';
|
import '../models/message_route_metadata.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
import 'package:latlong2/latlong.dart';
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
/// Service for persisting messages to local storage
|
/// Service for persisting messages to local storage
|
||||||
@@ -24,6 +25,10 @@ class MessageStorageService {
|
|||||||
static const String _legacyPathBytesKey = 'storedPathBytes';
|
static const String _legacyPathBytesKey = 'storedPathBytes';
|
||||||
static const int _maxStoredMessages = 1000; // Store up to 1000 messages
|
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
|
/// Save messages to persistent storage
|
||||||
Future<void> saveMessages(
|
Future<void> saveMessages(
|
||||||
List<Message> messages, {
|
List<Message> messages, {
|
||||||
@@ -31,6 +36,7 @@ class MessageStorageService {
|
|||||||
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
Map<String, MessageReceptionDetails> messageReceptionDetails = const {},
|
||||||
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
Map<String, MessageTransferDetails> messageTransferDetails = const {},
|
||||||
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
Map<String, MessageRouteMetadata> messageRouteMetadata = const {},
|
||||||
|
String? namespace,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
@@ -52,7 +58,10 @@ class MessageStorageService {
|
|||||||
: jsonList;
|
: jsonList;
|
||||||
|
|
||||||
final jsonString = jsonEncode(limitedList);
|
final jsonString = jsonEncode(limitedList);
|
||||||
await prefs.setString(_messagesKey, jsonString);
|
await prefs.setString(
|
||||||
|
_key(_messagesKey, namespace: namespace),
|
||||||
|
jsonString,
|
||||||
|
);
|
||||||
final retainedMessageIds = limitedList
|
final retainedMessageIds = limitedList
|
||||||
.map((entry) => entry['id'] as String)
|
.map((entry) => entry['id'] as String)
|
||||||
.toSet();
|
.toSet();
|
||||||
@@ -81,19 +90,19 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await prefs.setString(
|
await prefs.setString(
|
||||||
_messageContactLocationsKey,
|
_key(_messageContactLocationsKey, namespace: namespace),
|
||||||
jsonEncode(locationJson),
|
jsonEncode(locationJson),
|
||||||
);
|
);
|
||||||
await prefs.setString(
|
await prefs.setString(
|
||||||
_messageReceptionDetailsKey,
|
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||||
jsonEncode(receptionJson),
|
jsonEncode(receptionJson),
|
||||||
);
|
);
|
||||||
await prefs.setString(
|
await prefs.setString(
|
||||||
_messageTransferDetailsKey,
|
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||||
jsonEncode(transferJson),
|
jsonEncode(transferJson),
|
||||||
);
|
);
|
||||||
await prefs.setString(
|
await prefs.setString(
|
||||||
_messageRouteMetadataKey,
|
_key(_messageRouteMetadataKey, namespace: namespace),
|
||||||
jsonEncode(routeMetadataJson),
|
jsonEncode(routeMetadataJson),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -105,11 +114,14 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, MessageContactLocation>>
|
Future<Map<String, MessageContactLocation>> loadMessageContactLocations({
|
||||||
loadMessageContactLocations() async {
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messageContactLocationsKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messageContactLocationsKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
@@ -135,11 +147,14 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, MessageReceptionDetails>>
|
Future<Map<String, MessageReceptionDetails>> loadMessageReceptionDetails({
|
||||||
loadMessageReceptionDetails() async {
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messageReceptionDetailsKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messageReceptionDetailsKey, namespace: namespace),
|
||||||
|
);
|
||||||
final result = <String, MessageReceptionDetails>{};
|
final result = <String, MessageReceptionDetails>{};
|
||||||
if (jsonString != null && jsonString.isNotEmpty) {
|
if (jsonString != null && jsonString.isNotEmpty) {
|
||||||
final decoded = jsonDecode(jsonString);
|
final decoded = jsonDecode(jsonString);
|
||||||
@@ -155,12 +170,16 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final embeddedReceptionDetails = await _loadEmbeddedReceptionDetails();
|
final embeddedReceptionDetails = await _loadEmbeddedReceptionDetails(
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
embeddedReceptionDetails.forEach((messageId, snapshot) {
|
embeddedReceptionDetails.forEach((messageId, snapshot) {
|
||||||
result.putIfAbsent(messageId, () => snapshot);
|
result.putIfAbsent(messageId, () => snapshot);
|
||||||
});
|
});
|
||||||
|
|
||||||
final fallbackPathBytes = await _loadLegacyPathBytesFromMessages();
|
final fallbackPathBytes = await _loadLegacyPathBytesFromMessages(
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
fallbackPathBytes.forEach((messageId, pathBytes) {
|
fallbackPathBytes.forEach((messageId, pathBytes) {
|
||||||
result.putIfAbsent(
|
result.putIfAbsent(
|
||||||
messageId,
|
messageId,
|
||||||
@@ -177,11 +196,14 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, MessageTransferDetails>>
|
Future<Map<String, MessageTransferDetails>> loadMessageTransferDetails({
|
||||||
loadMessageTransferDetails() async {
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messageTransferDetailsKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messageTransferDetailsKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
@@ -207,10 +229,14 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata() async {
|
Future<Map<String, MessageRouteMetadata>> loadMessageRouteMetadata({
|
||||||
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messageRouteMetadataKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messageRouteMetadataKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
@@ -234,10 +260,12 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Load messages from persistent storage
|
/// Load messages from persistent storage
|
||||||
Future<List<Message>> loadMessages() async {
|
Future<List<Message>> loadMessages({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messagesKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messagesKey, namespace: namespace),
|
||||||
|
);
|
||||||
|
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
debugPrint('ℹ️ [MessageStorage] No stored messages found');
|
debugPrint('ℹ️ [MessageStorage] No stored messages found');
|
||||||
@@ -262,25 +290,35 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Clear all stored messages
|
/// Clear all stored messages
|
||||||
Future<void> clearMessages() async {
|
Future<void> clearMessages({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.remove(_messagesKey);
|
await prefs.remove(_key(_messagesKey, namespace: namespace));
|
||||||
await prefs.remove(_messageContactLocationsKey);
|
await prefs.remove(
|
||||||
await prefs.remove(_messageReceptionDetailsKey);
|
_key(_messageContactLocationsKey, namespace: namespace),
|
||||||
await prefs.remove(_messageTransferDetailsKey);
|
);
|
||||||
await prefs.remove(_messageRouteMetadataKey);
|
await prefs.remove(
|
||||||
await prefs.remove(_removedSarMarkerIdsKey);
|
_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');
|
debugPrint('✅ [MessageStorage] Cleared all stored messages');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [MessageStorage] Error clearing messages: $e');
|
debugPrint('❌ [MessageStorage] Error clearing messages: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Set<String>> loadRemovedSarMarkerIds() async {
|
Future<Set<String>> loadRemovedSarMarkerIds({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final ids = prefs.getStringList(_removedSarMarkerIdsKey) ?? const [];
|
final ids =
|
||||||
|
prefs.getStringList(
|
||||||
|
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||||
|
) ??
|
||||||
|
const [];
|
||||||
return ids.toSet();
|
return ids.toSet();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [MessageStorage] Error loading removed SAR marker IDs: $e');
|
debugPrint('❌ [MessageStorage] Error loading removed SAR marker IDs: $e');
|
||||||
@@ -288,10 +326,16 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> saveRemovedSarMarkerIds(Set<String> ids) async {
|
Future<void> saveRemovedSarMarkerIds(
|
||||||
|
Set<String> ids, {
|
||||||
|
String? namespace,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setStringList(_removedSarMarkerIdsKey, ids.toList()..sort());
|
await prefs.setStringList(
|
||||||
|
_key(_removedSarMarkerIdsKey, namespace: namespace),
|
||||||
|
ids.toList()..sort(),
|
||||||
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [MessageStorage] Saved ${ids.length} removed SAR marker IDs',
|
'✅ [MessageStorage] Saved ${ids.length} removed SAR marker IDs',
|
||||||
);
|
);
|
||||||
@@ -301,10 +345,12 @@ class MessageStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Get storage statistics
|
/// Get storage statistics
|
||||||
Future<Map<String, dynamic>> getStorageStats() async {
|
Future<Map<String, dynamic>> getStorageStats({String? namespace}) async {
|
||||||
try {
|
try {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messagesKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messagesKey, namespace: namespace),
|
||||||
|
);
|
||||||
|
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
return {'messageCount': 0, 'storageSizeBytes': 0, 'storageSizeKB': 0};
|
||||||
@@ -329,6 +375,8 @@ class MessageStorageService {
|
|||||||
Message message, {
|
Message message, {
|
||||||
MessageReceptionDetails? receptionDetails,
|
MessageReceptionDetails? receptionDetails,
|
||||||
}) {
|
}) {
|
||||||
|
final persistedPathBytes =
|
||||||
|
message.pathBytes ?? _pathBytesFromSnapshot(receptionDetails);
|
||||||
return {
|
return {
|
||||||
'id': message.id,
|
'id': message.id,
|
||||||
'messageType': message.messageType.name,
|
'messageType': message.messageType.name,
|
||||||
@@ -337,12 +385,16 @@ class MessageStorageService {
|
|||||||
: null,
|
: null,
|
||||||
'channelIdx': message.channelIdx,
|
'channelIdx': message.channelIdx,
|
||||||
'pathLen': message.pathLen,
|
'pathLen': message.pathLen,
|
||||||
|
'pathBytes': persistedPathBytes?.toList(),
|
||||||
'textType': message.textType.value,
|
'textType': message.textType.value,
|
||||||
'senderTimestamp': message.senderTimestamp,
|
'senderTimestamp': message.senderTimestamp,
|
||||||
'text': message.text,
|
'text': message.text,
|
||||||
'isSarMarker': message.isSarMarker,
|
'isSarMarker': message.isSarMarker,
|
||||||
'sarGpsLat': message.sarGpsCoordinates?.latitude,
|
'sarGpsLat': message.sarGpsCoordinates?.latitude,
|
||||||
'sarGpsLon': message.sarGpsCoordinates?.longitude,
|
'sarGpsLon': message.sarGpsCoordinates?.longitude,
|
||||||
|
'sarCustomMapLat': message.sarCustomMapPoint?.latitude,
|
||||||
|
'sarCustomMapLon': message.sarCustomMapPoint?.longitude,
|
||||||
|
'sarCustomMapId': message.sarCustomMapId,
|
||||||
'sarNotes': message.sarNotes,
|
'sarNotes': message.sarNotes,
|
||||||
'sarCustomEmoji': message.sarCustomEmoji,
|
'sarCustomEmoji': message.sarCustomEmoji,
|
||||||
'sarColorIndex': message.sarColorIndex,
|
'sarColorIndex': message.sarColorIndex,
|
||||||
@@ -395,10 +447,13 @@ class MessageStorageService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, MessageReceptionDetails>>
|
Future<Map<String, MessageReceptionDetails>> _loadEmbeddedReceptionDetails({
|
||||||
_loadEmbeddedReceptionDetails() async {
|
String? namespace,
|
||||||
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messagesKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messagesKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
@@ -421,9 +476,13 @@ class MessageStorageService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Map<String, List<int>>> _loadLegacyPathBytesFromMessages() async {
|
Future<Map<String, List<int>>> _loadLegacyPathBytesFromMessages({
|
||||||
|
String? namespace,
|
||||||
|
}) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final jsonString = prefs.getString(_messagesKey);
|
final jsonString = prefs.getString(
|
||||||
|
_key(_messagesKey, namespace: namespace),
|
||||||
|
);
|
||||||
if (jsonString == null || jsonString.isEmpty) {
|
if (jsonString == null || jsonString.isEmpty) {
|
||||||
return const {};
|
return const {};
|
||||||
}
|
}
|
||||||
@@ -465,14 +524,33 @@ class MessageStorageService {
|
|||||||
: null,
|
: null,
|
||||||
channelIdx: json['channelIdx'] as int?,
|
channelIdx: json['channelIdx'] as int?,
|
||||||
pathLen: json['pathLen'] 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),
|
textType: MessageTextType.fromValue(json['textType'] as int),
|
||||||
senderTimestamp: json['senderTimestamp'] as int,
|
senderTimestamp: json['senderTimestamp'] as int,
|
||||||
text: json['text'] as String,
|
text: json['text'] as String,
|
||||||
isSarMarker: json['isSarMarker'] as bool? ?? false,
|
isSarMarker: json['isSarMarker'] as bool? ?? false,
|
||||||
sarGpsCoordinates:
|
sarGpsCoordinates:
|
||||||
json['sarGpsLat'] != null && json['sarGpsLon'] != null
|
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,
|
: 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?,
|
sarNotes: json['sarNotes'] as String?,
|
||||||
sarCustomEmoji: json['sarCustomEmoji'] as String?,
|
sarCustomEmoji: json['sarCustomEmoji'] as String?,
|
||||||
sarColorIndex: json['sarColorIndex'] as int?,
|
sarColorIndex: json['sarColorIndex'] as int?,
|
||||||
@@ -562,4 +640,12 @@ class MessageStorageService {
|
|||||||
return null;
|
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 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
class MessagingRoutePreferences {
|
class MessagingRoutePreferences {
|
||||||
static const bool defaultAutoRouteRotationEnabled = false;
|
static const bool defaultAutoRouteRotationEnabled = false;
|
||||||
@@ -14,33 +15,49 @@ class MessagingRoutePreferences {
|
|||||||
|
|
||||||
static Future<bool> getAutoRouteRotationEnabled() async {
|
static Future<bool> getAutoRouteRotationEnabled() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool(_autoRouteRotationKey) ??
|
return prefs.getBool(
|
||||||
|
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||||
|
) ??
|
||||||
defaultAutoRouteRotationEnabled;
|
defaultAutoRouteRotationEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setAutoRouteRotationEnabled(bool enabled) async {
|
static Future<void> setAutoRouteRotationEnabled(bool enabled) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_autoRouteRotationKey, enabled);
|
await prefs.setBool(
|
||||||
|
ProfileStorageScope.scopedKey(_autoRouteRotationKey),
|
||||||
|
enabled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> getClearPathOnMaxRetry() async {
|
static Future<bool> getClearPathOnMaxRetry() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool(_clearPathOnMaxRetryKey) ?? defaultClearPathOnMaxRetry;
|
return prefs.getBool(
|
||||||
|
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||||
|
) ??
|
||||||
|
defaultClearPathOnMaxRetry;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setClearPathOnMaxRetry(bool enabled) async {
|
static Future<void> setClearPathOnMaxRetry(bool enabled) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setBool(_clearPathOnMaxRetryKey, enabled);
|
await prefs.setBool(
|
||||||
|
ProfileStorageScope.scopedKey(_clearPathOnMaxRetryKey),
|
||||||
|
enabled,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<bool> getNearestRelayFallbackEnabled() async {
|
static Future<bool> getNearestRelayFallbackEnabled() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
return prefs.getBool(_nearestRelayFallbackKey) ??
|
return prefs.getBool(
|
||||||
|
ProfileStorageScope.scopedKey(_nearestRelayFallbackKey),
|
||||||
|
) ??
|
||||||
defaultNearestRelayFallbackEnabled;
|
defaultNearestRelayFallbackEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
static Future<void> setNearestRelayFallbackEnabled(bool enabled) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
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/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.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 'package:timezone/data/latest_all.dart' as tz;
|
||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
import '../l10n/app_localizations.dart';
|
import '../l10n/app_localizations.dart';
|
||||||
@@ -13,14 +14,28 @@ class NotificationService {
|
|||||||
|
|
||||||
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
final FlutterLocalNotificationsPlugin _notificationsPlugin =
|
||||||
FlutterLocalNotificationsPlugin();
|
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 _isInitialized = false;
|
||||||
bool _permissionGranted = 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
|
// Notification IDs
|
||||||
static const int _sarNotificationId = 1000;
|
static const int _sarNotificationId = 1000;
|
||||||
static const int _messageNotificationId = 2000;
|
static const int _messageNotificationId = 2000;
|
||||||
static const int _updateNotificationId = 3000;
|
static const int _updateNotificationId = 3000;
|
||||||
|
static const int _batteryNotificationId = 4000;
|
||||||
|
static const int _discoveryNotificationId = 5000;
|
||||||
|
|
||||||
// Notification channels
|
// Notification channels
|
||||||
static const String _urgentChannelId = 'sar_urgent';
|
static const String _urgentChannelId = 'sar_urgent';
|
||||||
@@ -38,12 +53,32 @@ class NotificationService {
|
|||||||
static const String _updateChannelDescription =
|
static const String _updateChannelDescription =
|
||||||
'Notifications for available app updates';
|
'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
|
/// Initialize notification service
|
||||||
Future<void> initialize() async {
|
Future<void> initialize() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
debugPrint('📬 [NotificationService] Initializing...');
|
debugPrint('📬 [NotificationService] Initializing...');
|
||||||
|
WidgetsBinding.instance.addObserver(_LifecycleObserver(this));
|
||||||
|
_lifecycleState =
|
||||||
|
WidgetsBinding.instance.lifecycleState ?? AppLifecycleState.resumed;
|
||||||
|
|
||||||
// Initialize timezone data
|
// Initialize timezone data
|
||||||
tz.initializeTimeZones();
|
tz.initializeTimeZones();
|
||||||
@@ -72,8 +107,15 @@ class NotificationService {
|
|||||||
onDidReceiveNotificationResponse: _onNotificationResponse,
|
onDidReceiveNotificationResponse: _onNotificationResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final launchDetails = await _notificationsPlugin
|
||||||
|
.getNotificationAppLaunchDetails();
|
||||||
|
if (launchDetails?.didNotificationLaunchApp ?? false) {
|
||||||
|
_launchPayload = launchDetails?.notificationResponse?.payload;
|
||||||
|
}
|
||||||
|
|
||||||
// Request permissions
|
// Request permissions
|
||||||
await _requestPermissions();
|
await _requestPermissions();
|
||||||
|
await _loadPreferences();
|
||||||
|
|
||||||
// Create notification channels (Android)
|
// Create notification channels (Android)
|
||||||
await _createNotificationChannels();
|
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
|
/// Create notification channels for Android
|
||||||
Future<void> _createNotificationChannels() async {
|
Future<void> _createNotificationChannels() async {
|
||||||
try {
|
try {
|
||||||
@@ -177,9 +267,31 @@ class NotificationService {
|
|||||||
showBadge: true,
|
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(urgentChannel);
|
||||||
await androidPlugin.createNotificationChannel(messagesChannel);
|
await androidPlugin.createNotificationChannel(messagesChannel);
|
||||||
await androidPlugin.createNotificationChannel(updateChannel);
|
await androidPlugin.createNotificationChannel(updateChannel);
|
||||||
|
await androidPlugin.createNotificationChannel(batteryChannel);
|
||||||
|
await androidPlugin.createNotificationChannel(discoveryChannel);
|
||||||
debugPrint('✅ [NotificationService] Created notification channels');
|
debugPrint('✅ [NotificationService] Created notification channels');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('⚠️ [NotificationService] Error creating channels: $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
|
/// Show urgent notification for SAR marker
|
||||||
Future<void> showSarNotification({
|
Future<void> showSarNotification({
|
||||||
required SarMarkerType type,
|
required SarMarkerType type,
|
||||||
@@ -222,6 +340,16 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!_sarNotificationsEnabled) {
|
||||||
|
debugPrint('ℹ️ [NotificationService] SAR notifications disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_shouldSuppressForegroundNotifications()) {
|
||||||
|
debugPrint(
|
||||||
|
'ℹ️ [NotificationService] App in foreground, skipping SAR notification',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate unique notification ID based on timestamp
|
// Generate unique notification ID based on timestamp
|
||||||
@@ -394,6 +522,16 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!_messageNotificationsEnabled) {
|
||||||
|
debugPrint('ℹ️ [NotificationService] Message notifications disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_shouldSuppressForegroundNotifications()) {
|
||||||
|
debugPrint(
|
||||||
|
'ℹ️ [NotificationService] App in foreground, skipping message notification',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Generate unique notification ID based on timestamp
|
// Generate unique notification ID based on timestamp
|
||||||
@@ -547,6 +685,16 @@ class NotificationService {
|
|||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (!_updateNotificationsEnabled) {
|
||||||
|
debugPrint('ℹ️ [NotificationService] Update notifications disabled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_shouldSuppressForegroundNotifications()) {
|
||||||
|
debugPrint(
|
||||||
|
'ℹ️ [NotificationService] App in foreground, skipping update notification',
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Build notification title and body
|
// Build notification title and body
|
||||||
@@ -617,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
lib/services/profile_transport.dart
Normal file
10
lib/services/profile_transport.dart
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
abstract class ProfileTransport {
|
||||||
|
String get label;
|
||||||
|
}
|
||||||
|
|
||||||
|
class FileProfileTransport implements ProfileTransport {
|
||||||
|
const FileProfileTransport();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String get label => 'File';
|
||||||
|
}
|
||||||
499
lib/services/profile_workspace_coordinator.dart
Normal file
499
lib/services/profile_workspace_coordinator.dart
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:file_picker/file_picker.dart';
|
||||||
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
import '../models/config_profile.dart';
|
||||||
|
import '../providers/app_provider.dart';
|
||||||
|
import '../providers/channels_provider.dart';
|
||||||
|
import '../providers/connection_provider.dart';
|
||||||
|
import '../providers/contacts_provider.dart';
|
||||||
|
import '../providers/drawing_provider.dart';
|
||||||
|
import '../providers/map_provider.dart';
|
||||||
|
import '../providers/messages_provider.dart';
|
||||||
|
import '../providers/sensors_provider.dart';
|
||||||
|
import '../providers/voice_provider.dart';
|
||||||
|
import '../providers/image_provider.dart' as ip;
|
||||||
|
import 'app_config_snapshot_service.dart';
|
||||||
|
import 'contact_storage_service.dart';
|
||||||
|
import 'device_config_applicator.dart';
|
||||||
|
import 'message_storage_service.dart';
|
||||||
|
import 'profile_manager.dart';
|
||||||
|
import 'profile_device_key_resolver.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
import 'map_workspace_snapshot_service.dart';
|
||||||
|
|
||||||
|
class ProfileWorkspaceCoordinator {
|
||||||
|
ProfileWorkspaceCoordinator({
|
||||||
|
required this.profileManager,
|
||||||
|
required this.connectionProvider,
|
||||||
|
required this.contactsProvider,
|
||||||
|
required this.messagesProvider,
|
||||||
|
required this.sensorsProvider,
|
||||||
|
required this.mapProvider,
|
||||||
|
required this.drawingProvider,
|
||||||
|
required this.channelsProvider,
|
||||||
|
required this.voiceProvider,
|
||||||
|
required this.imageProvider,
|
||||||
|
required this.appProvider,
|
||||||
|
AppConfigSnapshotService? appConfigSnapshotService,
|
||||||
|
MapWorkspaceSnapshotService? mapWorkspaceSnapshotService,
|
||||||
|
DeviceConfigApplicator? deviceConfigApplicator,
|
||||||
|
MessageStorageService? messageStorageService,
|
||||||
|
ContactStorageService? contactStorageService,
|
||||||
|
}) : _appConfigSnapshotService =
|
||||||
|
appConfigSnapshotService ?? AppConfigSnapshotService(),
|
||||||
|
_mapWorkspaceSnapshotService =
|
||||||
|
mapWorkspaceSnapshotService ?? MapWorkspaceSnapshotService(),
|
||||||
|
_deviceConfigApplicator =
|
||||||
|
deviceConfigApplicator ?? DeviceConfigApplicator(),
|
||||||
|
_messageStorageService =
|
||||||
|
messageStorageService ?? MessageStorageService(),
|
||||||
|
_contactStorageService =
|
||||||
|
contactStorageService ?? ContactStorageService();
|
||||||
|
|
||||||
|
final ProfileManager profileManager;
|
||||||
|
final ConnectionProvider connectionProvider;
|
||||||
|
final ContactsProvider contactsProvider;
|
||||||
|
final MessagesProvider messagesProvider;
|
||||||
|
final SensorsProvider sensorsProvider;
|
||||||
|
final MapProvider mapProvider;
|
||||||
|
final DrawingProvider drawingProvider;
|
||||||
|
final ChannelsProvider channelsProvider;
|
||||||
|
final VoiceProvider voiceProvider;
|
||||||
|
final ip.ImageProvider imageProvider;
|
||||||
|
final AppProvider appProvider;
|
||||||
|
final AppConfigSnapshotService _appConfigSnapshotService;
|
||||||
|
final MapWorkspaceSnapshotService _mapWorkspaceSnapshotService;
|
||||||
|
final DeviceConfigApplicator _deviceConfigApplicator;
|
||||||
|
final MessageStorageService _messageStorageService;
|
||||||
|
final ContactStorageService _contactStorageService;
|
||||||
|
Future<void>? _deviceProfileSyncFuture;
|
||||||
|
|
||||||
|
Future<void> setProfilesEnabled(bool enabled) async {
|
||||||
|
final wasEnabled = profileManager.profilesEnabled;
|
||||||
|
if (wasEnabled && !enabled) {
|
||||||
|
await _saveActiveCustomProfileSnapshot();
|
||||||
|
} else {
|
||||||
|
await _persistCurrentState();
|
||||||
|
}
|
||||||
|
await profileManager.setProfilesEnabled(enabled);
|
||||||
|
ProfileStorageScope.setScope(
|
||||||
|
profilesEnabled: enabled,
|
||||||
|
activeProfileId: enabled ? profileManager.activeProfileId : 'default',
|
||||||
|
);
|
||||||
|
if (enabled) {
|
||||||
|
await _ensureProfileForCurrentDevice();
|
||||||
|
if (wasEnabled) {
|
||||||
|
await openProfile(profileManager.activeProfileId);
|
||||||
|
} else {
|
||||||
|
await _switchRuntimeScope(profileManager.activeProfileId);
|
||||||
|
final profile = await resolveProfile(profileManager.activeProfileId);
|
||||||
|
await _appConfigSnapshotService.apply(
|
||||||
|
profile.sections.appSettings,
|
||||||
|
appProvider,
|
||||||
|
);
|
||||||
|
await _mapWorkspaceSnapshotService.apply(
|
||||||
|
profile.sections.mapWorkspace,
|
||||||
|
mapProvider: mapProvider,
|
||||||
|
drawingProvider: drawingProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await _switchRuntimeScope('default');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile> snapshotCurrentProfile({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
String? notes,
|
||||||
|
}) async {
|
||||||
|
final deviceSections = _deviceConfigApplicator.capture(
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
channelsProvider: channelsProvider,
|
||||||
|
);
|
||||||
|
final appSettings = await _appConfigSnapshotService.capture(appProvider);
|
||||||
|
final mapWorkspace = await _mapWorkspaceSnapshotService.capture(
|
||||||
|
mapProvider: mapProvider,
|
||||||
|
drawingProvider: drawingProvider,
|
||||||
|
);
|
||||||
|
final now = DateTime.now();
|
||||||
|
return ConfigProfile(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
notes: notes,
|
||||||
|
sections: ConfigProfileSections(
|
||||||
|
deviceConfig: deviceSections.deviceConfig,
|
||||||
|
channels: deviceSections.channels,
|
||||||
|
appSettings: appSettings,
|
||||||
|
mapWorkspace: mapWorkspace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile> createProfileFromCurrent({
|
||||||
|
required String name,
|
||||||
|
String? notes,
|
||||||
|
}) async {
|
||||||
|
final profileId = 'profile_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
final profile = await snapshotCurrentProfile(
|
||||||
|
id: profileId,
|
||||||
|
name: name,
|
||||||
|
notes: notes,
|
||||||
|
);
|
||||||
|
await _initializeEmptyStorageNamespace(profileId);
|
||||||
|
await profileManager.upsertProfile(profile);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile> duplicateProfile(ConfigProfile source) async {
|
||||||
|
final duplicate = await snapshotCurrentProfile(
|
||||||
|
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
name: '${source.name} Copy',
|
||||||
|
notes: source.notes,
|
||||||
|
);
|
||||||
|
final sourceNamespace = source.id == ConfigProfile.defaultProfileId
|
||||||
|
? null
|
||||||
|
: source.id;
|
||||||
|
await _copyStorageNamespace(sourceNamespace, duplicate.id);
|
||||||
|
await profileManager.upsertProfile(
|
||||||
|
duplicate.copyWith(
|
||||||
|
sections: source.id == profileManager.activeProfileId
|
||||||
|
? duplicate.sections
|
||||||
|
: source.sections,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return duplicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> renameProfile(ConfigProfile profile, String name) async {
|
||||||
|
await profileManager.upsertProfile(
|
||||||
|
profile.copyWith(name: name, updatedAt: DateTime.now()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteProfile(ConfigProfile profile) async {
|
||||||
|
if (profile.isDefault) return;
|
||||||
|
if (profile.id == profileManager.activeProfileId) {
|
||||||
|
await openProfile(ConfigProfile.defaultProfileId);
|
||||||
|
}
|
||||||
|
await _messageStorageService.clearMessages(namespace: profile.id);
|
||||||
|
await _contactStorageService.clearContacts(namespace: profile.id);
|
||||||
|
await _contactStorageService.clearContactGroups(namespace: profile.id);
|
||||||
|
await _contactStorageService.clearPendingAdverts(namespace: profile.id);
|
||||||
|
await profileManager.deleteProfile(profile.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> openProfile(String profileId) async {
|
||||||
|
final deviceKey = _currentDeviceProfileKey;
|
||||||
|
await _saveActiveCustomProfileSnapshot();
|
||||||
|
await connectionProvider.disconnect();
|
||||||
|
await profileManager.setActiveProfileIdForDevice(
|
||||||
|
profileId,
|
||||||
|
deviceKey: deviceKey,
|
||||||
|
);
|
||||||
|
await _switchRuntimeScope(profileId);
|
||||||
|
|
||||||
|
final profile = await resolveProfile(profileId);
|
||||||
|
await _appConfigSnapshotService.apply(
|
||||||
|
profile.sections.appSettings,
|
||||||
|
appProvider,
|
||||||
|
);
|
||||||
|
await _mapWorkspaceSnapshotService.apply(
|
||||||
|
profile.sections.mapWorkspace,
|
||||||
|
mapProvider: mapProvider,
|
||||||
|
drawingProvider: drawingProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> applyProfile(String profileId) async {
|
||||||
|
await openProfile(profileId);
|
||||||
|
final profile = await resolveProfile(profileId);
|
||||||
|
await _deviceConfigApplicator.apply(
|
||||||
|
profile,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
channelsProvider: channelsProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile> resolveProfile(String profileId) async {
|
||||||
|
if (profileId == ConfigProfile.defaultProfileId) {
|
||||||
|
return snapshotCurrentProfile(id: profileId, name: 'Default');
|
||||||
|
}
|
||||||
|
return profileManager.getProfile(profileId) ??
|
||||||
|
await snapshotCurrentProfile(id: profileId, name: 'Unknown');
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> exportProfile(ConfigProfile profile) async {
|
||||||
|
final resolved = profile.id == ConfigProfile.defaultProfileId
|
||||||
|
? await snapshotCurrentProfile(id: profile.id, name: profile.name)
|
||||||
|
: profile;
|
||||||
|
final tempDir = await getTemporaryDirectory();
|
||||||
|
final file = File(
|
||||||
|
'${tempDir.path}/${resolved.name.replaceAll(' ', '_').toLowerCase()}_${resolved.id}.meshcore_profile.json',
|
||||||
|
);
|
||||||
|
await file.writeAsString(
|
||||||
|
const JsonEncoder.withIndent(' ').convert(resolved.toJson()),
|
||||||
|
);
|
||||||
|
await SharePlus.instance.share(ShareParams(files: [XFile(file.path)]));
|
||||||
|
await profileManager.recordTransfer(
|
||||||
|
ProfileTransferRecord(
|
||||||
|
profileId: resolved.id,
|
||||||
|
direction: 'export',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
detail: 'Shared as file',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile?> importProfileFromFile() async {
|
||||||
|
final result = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ['json'],
|
||||||
|
withData: true,
|
||||||
|
);
|
||||||
|
if (result == null || result.files.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
final file = result.files.first;
|
||||||
|
if (file.bytes == null) return null;
|
||||||
|
|
||||||
|
final decoded = jsonDecode(utf8.decode(file.bytes!));
|
||||||
|
if (decoded is! Map<String, dynamic>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final imported = ConfigProfile.fromJson(decoded).copyWith(
|
||||||
|
id: 'profile_${DateTime.now().millisecondsSinceEpoch}',
|
||||||
|
updatedAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
await profileManager.upsertProfile(imported);
|
||||||
|
await profileManager.recordTransfer(
|
||||||
|
ProfileTransferRecord(
|
||||||
|
profileId: imported.id,
|
||||||
|
direction: 'import',
|
||||||
|
timestamp: DateTime.now(),
|
||||||
|
detail: file.name,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return imported;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> syncActiveProfileForCurrentDevice() async {
|
||||||
|
if (!profileManager.profilesEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final inFlightSync = _deviceProfileSyncFuture;
|
||||||
|
if (inFlightSync != null) {
|
||||||
|
await inFlightSync;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final syncFuture = _syncActiveProfileForCurrentDeviceInternal();
|
||||||
|
_deviceProfileSyncFuture = syncFuture;
|
||||||
|
try {
|
||||||
|
await syncFuture;
|
||||||
|
} finally {
|
||||||
|
if (identical(_deviceProfileSyncFuture, syncFuture)) {
|
||||||
|
_deviceProfileSyncFuture = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _syncActiveProfileForCurrentDeviceInternal() async {
|
||||||
|
final deviceKey = _currentDeviceProfileKey;
|
||||||
|
if (deviceKey == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final profile = await _ensureProfileForCurrentDevice();
|
||||||
|
final targetProfileId = profile.id;
|
||||||
|
if (targetProfileId == profileManager.activeProfileId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _persistCurrentState();
|
||||||
|
await profileManager.setActiveProfileIdForDevice(
|
||||||
|
targetProfileId,
|
||||||
|
deviceKey: deviceKey,
|
||||||
|
);
|
||||||
|
await _switchRuntimeScope(targetProfileId);
|
||||||
|
|
||||||
|
await _appConfigSnapshotService.apply(
|
||||||
|
profile.sections.appSettings,
|
||||||
|
appProvider,
|
||||||
|
);
|
||||||
|
await _mapWorkspaceSnapshotService.apply(
|
||||||
|
profile.sections.mapWorkspace,
|
||||||
|
mapProvider: mapProvider,
|
||||||
|
drawingProvider: drawingProvider,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<ConfigProfile> _ensureProfileForCurrentDevice() async {
|
||||||
|
final deviceKey = _currentDeviceProfileKey;
|
||||||
|
final targetProfileId = profileManager.profileIdForDevice(deviceKey);
|
||||||
|
final existingProfile = profileManager.getProfile(targetProfileId);
|
||||||
|
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||||
|
existingProfile != null) {
|
||||||
|
return existingProfile;
|
||||||
|
}
|
||||||
|
if (profileManager.hasProfileForDevice(deviceKey) &&
|
||||||
|
targetProfileId == ConfigProfile.defaultProfileId) {
|
||||||
|
return ConfigProfile.defaultProfile();
|
||||||
|
}
|
||||||
|
if (deviceKey == null) {
|
||||||
|
return await resolveProfile(profileManager.activeProfileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
final profile = await createProfileFromCurrent(
|
||||||
|
name: _buildDeviceProfileName(),
|
||||||
|
);
|
||||||
|
await profileManager.setActiveProfileIdForDevice(
|
||||||
|
profile.id,
|
||||||
|
deviceKey: deviceKey,
|
||||||
|
);
|
||||||
|
return profile;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _buildDeviceProfileName() {
|
||||||
|
final deviceInfo = connectionProvider.deviceInfo;
|
||||||
|
final name = deviceInfo.selfName?.trim();
|
||||||
|
if (name != null && name.isNotEmpty) {
|
||||||
|
return 'Device ${_sanitizeProfileLabel(name)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
final displayName = deviceInfo.displayName?.trim();
|
||||||
|
if (displayName != null && displayName.isNotEmpty) {
|
||||||
|
return 'Device ${_sanitizeProfileLabel(displayName)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
final deviceId = deviceInfo.deviceId?.trim();
|
||||||
|
if (deviceId != null && deviceId.isNotEmpty) {
|
||||||
|
return 'Device ${_sanitizeProfileLabel(deviceId)}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Device Profile';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _sanitizeProfileLabel(String value) {
|
||||||
|
return value.replaceAll(RegExp(r'\s+'), ' ').trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
String? get _currentDeviceProfileKey => ProfileDeviceKeyResolver.resolve(
|
||||||
|
deviceInfo: connectionProvider.deviceInfo,
|
||||||
|
connectionMode: connectionProvider.connectionMode,
|
||||||
|
);
|
||||||
|
|
||||||
|
Future<void> _switchRuntimeScope(String profileId) async {
|
||||||
|
final runtimeProfilesEnabled = profileManager.profilesEnabled;
|
||||||
|
ProfileStorageScope.setScope(
|
||||||
|
profilesEnabled: runtimeProfilesEnabled,
|
||||||
|
activeProfileId: profileId,
|
||||||
|
);
|
||||||
|
await messagesProvider.reloadFromStorage(
|
||||||
|
namespace: ProfileStorageScope.effectiveNamespace,
|
||||||
|
);
|
||||||
|
await contactsProvider.reloadFromStorage(
|
||||||
|
namespace: ProfileStorageScope.effectiveNamespace,
|
||||||
|
devicePublicKey: connectionProvider.deviceInfo.publicKey,
|
||||||
|
);
|
||||||
|
await sensorsProvider.reloadProfileScopedState();
|
||||||
|
await drawingProvider.reloadProfileScopedState();
|
||||||
|
await mapProvider.reloadProfileScopedState();
|
||||||
|
await voiceProvider.reloadProfileScopedState();
|
||||||
|
await imageProvider.reloadProfileScopedState();
|
||||||
|
await appProvider.reloadProfileScopedSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistCurrentState() async {
|
||||||
|
await messagesProvider.persistNow();
|
||||||
|
await contactsProvider.persistNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _saveActiveCustomProfileSnapshot() async {
|
||||||
|
if (profileManager.activeProfileId == ConfigProfile.defaultProfileId) {
|
||||||
|
await _persistCurrentState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final current = profileManager.getProfile(profileManager.activeProfileId);
|
||||||
|
if (current == null) {
|
||||||
|
await _persistCurrentState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final snapshot = await snapshotCurrentProfile(
|
||||||
|
id: current.id,
|
||||||
|
name: current.name,
|
||||||
|
notes: current.notes,
|
||||||
|
);
|
||||||
|
await profileManager.upsertProfile(snapshot);
|
||||||
|
await _persistCurrentState();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _copyStorageNamespace(
|
||||||
|
String? sourceNamespace,
|
||||||
|
String targetNamespace,
|
||||||
|
) async {
|
||||||
|
final messages = await _messageStorageService.loadMessages(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
final contactLocations = await _messageStorageService
|
||||||
|
.loadMessageContactLocations(namespace: sourceNamespace);
|
||||||
|
final receptionDetails = await _messageStorageService
|
||||||
|
.loadMessageReceptionDetails(namespace: sourceNamespace);
|
||||||
|
final transferDetails = await _messageStorageService
|
||||||
|
.loadMessageTransferDetails(namespace: sourceNamespace);
|
||||||
|
final routeMetadata = await _messageStorageService.loadMessageRouteMetadata(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
final removedIds = await _messageStorageService.loadRemovedSarMarkerIds(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
await _messageStorageService.saveMessages(
|
||||||
|
messages,
|
||||||
|
messageContactLocations: contactLocations,
|
||||||
|
messageReceptionDetails: receptionDetails,
|
||||||
|
messageTransferDetails: transferDetails,
|
||||||
|
messageRouteMetadata: routeMetadata,
|
||||||
|
namespace: targetNamespace,
|
||||||
|
);
|
||||||
|
await _messageStorageService.saveRemovedSarMarkerIds(
|
||||||
|
removedIds,
|
||||||
|
namespace: targetNamespace,
|
||||||
|
);
|
||||||
|
|
||||||
|
final contacts = await _contactStorageService.loadContacts(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
final groups = await _contactStorageService.loadContactGroups(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
final pending = await _contactStorageService.loadPendingAdverts(
|
||||||
|
namespace: sourceNamespace,
|
||||||
|
);
|
||||||
|
await _contactStorageService.saveContacts(
|
||||||
|
contacts,
|
||||||
|
namespace: targetNamespace,
|
||||||
|
);
|
||||||
|
await _contactStorageService.saveContactGroups(
|
||||||
|
groups,
|
||||||
|
namespace: targetNamespace,
|
||||||
|
);
|
||||||
|
await _contactStorageService.savePendingAdverts(
|
||||||
|
pending,
|
||||||
|
namespace: targetNamespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _initializeEmptyStorageNamespace(String namespace) async {
|
||||||
|
await _messageStorageService.clearMessages(namespace: namespace);
|
||||||
|
await _contactStorageService.clearContacts(namespace: namespace);
|
||||||
|
await _contactStorageService.clearContactGroups(namespace: namespace);
|
||||||
|
await _contactStorageService.clearPendingAdverts(namespace: namespace);
|
||||||
|
}
|
||||||
|
}
|
||||||
54
lib/services/profiles_feature_service.dart
Normal file
54
lib/services/profiles_feature_service.dart
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class ProfilesFeatureService {
|
||||||
|
static const String enabledKey = 'profiles_enabled';
|
||||||
|
|
||||||
|
static Future<bool> isEnabled() async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
return prefs.getBool(enabledKey) ?? true;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> setEnabled(bool enabled) async {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(enabledKey, enabled);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ProfileStorageScope {
|
||||||
|
static bool _profilesEnabled = true;
|
||||||
|
static String _activeProfileId = 'default';
|
||||||
|
|
||||||
|
static Future<void> bootstrap({
|
||||||
|
required bool profilesEnabled,
|
||||||
|
required String activeProfileId,
|
||||||
|
}) async {
|
||||||
|
_profilesEnabled = profilesEnabled;
|
||||||
|
_activeProfileId = activeProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void setScope({
|
||||||
|
required bool profilesEnabled,
|
||||||
|
required String activeProfileId,
|
||||||
|
}) {
|
||||||
|
_profilesEnabled = profilesEnabled;
|
||||||
|
_activeProfileId = activeProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool get profilesEnabled => _profilesEnabled;
|
||||||
|
static String get activeProfileId => _activeProfileId;
|
||||||
|
|
||||||
|
static String? get effectiveNamespace {
|
||||||
|
if (!_profilesEnabled || _activeProfileId == 'default') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return _activeProfileId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static String scopedKey(String baseKey, {String? namespace}) {
|
||||||
|
final scope = namespace ?? effectiveNamespace;
|
||||||
|
if (scope == null || scope.isEmpty) {
|
||||||
|
return baseKey;
|
||||||
|
}
|
||||||
|
return 'profile.$scope.$baseKey';
|
||||||
|
}
|
||||||
|
}
|
||||||
48
lib/services/relay_candidate_sorter.dart
Normal file
48
lib/services/relay_candidate_sorter.dart
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../models/contact.dart';
|
||||||
|
|
||||||
|
class RelayCandidateSorter {
|
||||||
|
const RelayCandidateSorter();
|
||||||
|
|
||||||
|
List<Contact> sortByDistanceFromSelf(
|
||||||
|
List<Contact> contacts, {
|
||||||
|
required LatLng? selfPoint,
|
||||||
|
}) {
|
||||||
|
final sorted = List<Contact>.from(contacts);
|
||||||
|
sorted.sort((a, b) {
|
||||||
|
if (selfPoint != null) {
|
||||||
|
final distanceCompare = _distanceFrom(
|
||||||
|
selfPoint,
|
||||||
|
a,
|
||||||
|
).compareTo(_distanceFrom(selfPoint, b));
|
||||||
|
if (distanceCompare != 0) {
|
||||||
|
return distanceCompare;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final nameCompare = a.displayName.compareTo(b.displayName);
|
||||||
|
if (nameCompare != 0) {
|
||||||
|
return nameCompare;
|
||||||
|
}
|
||||||
|
|
||||||
|
return a.publicKeyHex.compareTo(b.publicKeyHex);
|
||||||
|
});
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
|
||||||
|
double _distanceFrom(LatLng selfPoint, Contact contact) {
|
||||||
|
final location = contact.displayLocation;
|
||||||
|
if (location == null) {
|
||||||
|
return double.infinity;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Geolocator.distanceBetween(
|
||||||
|
selfPoint.latitude,
|
||||||
|
selfPoint.longitude,
|
||||||
|
location.latitude,
|
||||||
|
location.longitude,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'profiles_feature_service.dart';
|
||||||
|
|
||||||
class RouteHashPreferences {
|
class RouteHashPreferences {
|
||||||
static const String _hashSizeKey = 'route_hash_size';
|
static const String _hashSizeKey = 'route_hash_size';
|
||||||
@@ -6,21 +7,28 @@ class RouteHashPreferences {
|
|||||||
|
|
||||||
static Future<int> getHashSize() async {
|
static Future<int> getHashSize() async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
final value = prefs.getInt(_hashSizeKey) ?? defaultHashSize;
|
final value =
|
||||||
|
prefs.getInt(ProfileStorageScope.scopedKey(_hashSizeKey)) ??
|
||||||
|
defaultHashSize;
|
||||||
return _normalize(value);
|
return _normalize(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<void> setHashSize(int value) async {
|
static Future<void> setHashSize(int value) async {
|
||||||
final prefs = await SharedPreferences.getInstance();
|
final prefs = await SharedPreferences.getInstance();
|
||||||
await prefs.setInt(_hashSizeKey, _normalize(value));
|
await prefs.setInt(
|
||||||
|
ProfileStorageScope.scopedKey(_hashSizeKey),
|
||||||
|
_normalize(value),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int normalizeSync(int value) => _normalize(value);
|
static int normalizeSync(int value) => _normalize(value);
|
||||||
|
|
||||||
|
/// Valid hash sizes per the MeshCore protocol.
|
||||||
|
/// The path encoding uses 2 bits: 00=1B, 01=2B, 10=3B, 11=4B.
|
||||||
|
static const List<int> supportedSizes = [1, 2, 3];
|
||||||
|
|
||||||
static int _normalize(int value) {
|
static int _normalize(int value) {
|
||||||
if (value < 1 || value > 3) {
|
if (supportedSizes.contains(value)) return value;
|
||||||
return defaultHashSize;
|
return defaultHashSize;
|
||||||
}
|
|
||||||
return value;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
50
lib/services/serial/serial_transport.dart
Normal file
50
lib/services/serial/serial_transport.dart
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import 'package:meshcore_client/meshcore_client.dart';
|
||||||
|
|
||||||
|
import 'serial_transport_stub.dart'
|
||||||
|
if (dart.library.io) 'serial_transport_io.dart'
|
||||||
|
if (dart.library.js_interop) 'serial_transport_web.dart';
|
||||||
|
|
||||||
|
const int kSerialBaudRate = 115200;
|
||||||
|
|
||||||
|
class SerialDeviceInfo {
|
||||||
|
final String id;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final Object handle;
|
||||||
|
|
||||||
|
const SerialDeviceInfo({
|
||||||
|
required this.id,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
required this.handle,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class SerialConnection {
|
||||||
|
final MeshCoreSerialService service;
|
||||||
|
final String deviceId;
|
||||||
|
final String deviceName;
|
||||||
|
final Future<void> Function() disconnect;
|
||||||
|
|
||||||
|
const SerialConnection({
|
||||||
|
required this.service,
|
||||||
|
required this.deviceId,
|
||||||
|
required this.deviceName,
|
||||||
|
required this.disconnect,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract class SerialTransport {
|
||||||
|
bool get isSupported;
|
||||||
|
bool get canRequestDevice;
|
||||||
|
String get actionLabel;
|
||||||
|
String get emptyStateTitle;
|
||||||
|
String get unsupportedMessage;
|
||||||
|
|
||||||
|
Future<List<SerialDeviceInfo>> listDevices();
|
||||||
|
Future<SerialDeviceInfo?> requestDevice();
|
||||||
|
Future<SerialConnection> connect(SerialDeviceInfo device);
|
||||||
|
void dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
SerialTransport createSerialTransport() => createSerialTransportImpl();
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user