Compare commits
173 Commits
v2026.0314
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7b6143a57 | ||
|
|
3c08e150ef | ||
|
|
3283d4ca20 | ||
|
|
bdbceece75 | ||
|
|
365bd76cbc | ||
|
|
ba27843166 | ||
|
|
3e1113035d | ||
|
|
89359c5e34 | ||
|
|
0406b59d3b | ||
|
|
c176ed0775 | ||
|
|
6062f91c2d | ||
|
|
e1400cf1ec | ||
|
|
b040a8bd3a | ||
|
|
c847bd6465 | ||
|
|
132a576785 | ||
|
|
9b8b1bf473 | ||
|
|
0008eff1b8 | ||
|
|
2fe125d6ba | ||
|
|
5834454087 | ||
|
|
73e78691ed | ||
|
|
d067095345 | ||
|
|
0b463f6454 | ||
|
|
a472b0c05c | ||
|
|
4181b33da1 | ||
|
|
f9dfdc2340 | ||
|
|
b333b3737e | ||
|
|
7245d76e17 | ||
|
|
7a76ba97f8 | ||
|
|
d00198c829 | ||
|
|
e6cd59e96f | ||
|
|
b863cda139 | ||
|
|
4bd7f36b98 | ||
|
|
44a668df93 | ||
|
|
e5514b4ef9 | ||
|
|
d92e52c5c2 | ||
|
|
daa88fb471 | ||
|
|
c182813c35 | ||
|
|
81944bfd3c | ||
|
|
43eb857663 | ||
|
|
faa396769e | ||
|
|
1a35b790a7 | ||
|
|
573921e002 | ||
|
|
14a41b210e | ||
|
|
56a7fa2fc6 | ||
|
|
7f4b4c4560 | ||
|
|
7f7f4aa975 | ||
|
|
1baab06627 | ||
|
|
5320fa1a2b | ||
|
|
512b64db9f | ||
|
|
4690ceead1 | ||
|
|
d431a79aa8 | ||
|
|
d66bbca82c | ||
|
|
d2fee94678 | ||
|
|
61a856e84c | ||
|
|
5feb07aad7 | ||
|
|
59931f402a | ||
|
|
0cd563063a | ||
|
|
9244368b49 | ||
|
|
c089db5c62 | ||
|
|
7c1bc3b84f | ||
|
|
724f4b38e8 | ||
|
|
caefbdbcef | ||
|
|
79f8f8dd04 | ||
|
|
e4eb40cd34 | ||
|
|
7c9dfd6f63 | ||
|
|
3e8d4c2d11 | ||
|
|
3a28acac2d | ||
|
|
5ee03d4b2f | ||
|
|
3dfdbd7700 | ||
|
|
043faea06a | ||
|
|
05ddb6841a | ||
|
|
3392e5f9c1 | ||
|
|
7df750c8d5 | ||
|
|
e09d418084 | ||
|
|
96aeec1301 | ||
|
|
6978f24440 | ||
|
|
ba0f1fc141 | ||
|
|
28a9235168 | ||
|
|
3e3c9a34d5 | ||
|
|
e85dccea35 | ||
|
|
019224d955 | ||
|
|
4c048a5a50 | ||
|
|
2b42e7dd15 | ||
|
|
7ea6334f02 | ||
|
|
6f0a66dc68 | ||
|
|
a26ae96acd | ||
|
|
a6dc137e51 | ||
|
|
c2501442e3 | ||
|
|
71eac21df9 | ||
|
|
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 |
95
.github/workflows/build-artifacts.yml
vendored
@@ -26,7 +26,7 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Java
|
- name: Setup Java
|
||||||
uses: actions/setup-java@v4
|
uses: actions/setup-java@v5
|
||||||
with:
|
with:
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
java-version: "17"
|
java-version: "17"
|
||||||
@@ -40,6 +40,17 @@ 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: Setup Ruby
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: "3.3"
|
||||||
|
bundler-cache: true
|
||||||
|
working-directory: android
|
||||||
|
|
||||||
- name: Cache Android NDK
|
- name: Cache Android NDK
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -52,22 +63,76 @@ jobs:
|
|||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
|
- name: Validate Android release secrets
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
run: |
|
||||||
|
test -n "$ANDROID_KEYSTORE_BASE64"
|
||||||
|
test -n "$ANDROID_KEYSTORE_PASSWORD"
|
||||||
|
test -n "$ANDROID_KEY_ALIAS"
|
||||||
|
test -n "$ANDROID_KEY_PASSWORD"
|
||||||
|
test -n "$GOOGLE_PLAY_SERVICE_ACCOUNT_JSON"
|
||||||
|
|
||||||
|
- name: Create Android signing files
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||||
|
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||||
|
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||||
|
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||||
|
run: |
|
||||||
|
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/release-keystore.jks
|
||||||
|
cat <<EOF > android/key.properties
|
||||||
|
storePassword=$ANDROID_KEYSTORE_PASSWORD
|
||||||
|
keyPassword=$ANDROID_KEY_PASSWORD
|
||||||
|
keyAlias=$ANDROID_KEY_ALIAS
|
||||||
|
storeFile=../release-keystore.jks
|
||||||
|
EOF
|
||||||
|
|
||||||
- name: Build APK
|
- name: Build APK
|
||||||
run: flutter build apk --release
|
run: flutter build apk --release
|
||||||
|
|
||||||
- name: Prepare APK artifact
|
- name: Build Play release AAB
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
run: flutter build appbundle --release
|
||||||
|
|
||||||
|
- name: Prepare Android artifacts
|
||||||
run: |
|
run: |
|
||||||
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
RAW_TAG="${{ github.event.release.tag_name || github.ref_name }}"
|
||||||
TAG="${RAW_TAG//\//-}"
|
TAG="${RAW_TAG//\//-}"
|
||||||
cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk"
|
cp build/app/outputs/flutter-apk/app-release.apk "${APP_NAME}-${TAG}-android.apk"
|
||||||
|
if [ -f build/app/outputs/bundle/release/app-release.aab ]; then
|
||||||
|
cp build/app/outputs/bundle/release/app-release.aab "${APP_NAME}-${TAG}-play.aab"
|
||||||
|
fi
|
||||||
|
|
||||||
- name: Upload APK artifact
|
- name: Upload Android artifacts
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: release-android
|
name: release-android
|
||||||
path: "${{ env.APP_NAME }}-*-android.apk"
|
path: |
|
||||||
|
${{ env.APP_NAME }}-*-android.apk
|
||||||
|
${{ env.APP_NAME }}-*-play.aab
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
- name: Upload Play release to internal testing
|
||||||
|
if: github.event_name == 'release'
|
||||||
|
env:
|
||||||
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
SKIP_ANDROID_BUILD: "1"
|
||||||
|
run: cd android && bundle exec fastlane android internal
|
||||||
|
|
||||||
|
- name: Upload Play release to production
|
||||||
|
if: github.event_name == 'release' && !github.event.release.prerelease
|
||||||
|
env:
|
||||||
|
GOOGLE_PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
|
||||||
|
SKIP_ANDROID_BUILD: "1"
|
||||||
|
run: cd android && bundle exec fastlane android production
|
||||||
|
|
||||||
build-linux:
|
build-linux:
|
||||||
name: Build Linux App
|
name: Build Linux App
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -95,6 +160,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 +200,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 +245,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 +255,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 +293,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 +352,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
|
||||||
@@ -280,7 +363,7 @@ jobs:
|
|||||||
run: flutter pub get
|
run: flutter pub get
|
||||||
|
|
||||||
- name: Build web release
|
- name: Build web release
|
||||||
run: flutter build web --release --base-href /meshcore-sar/
|
run: flutter build web --wasm --release --base-href /meshcore-sar/
|
||||||
|
|
||||||
- name: Upload pages artifact
|
- name: Upload pages artifact
|
||||||
uses: actions/upload-pages-artifact@v3
|
uses: actions/upload-pages-artifact@v3
|
||||||
@@ -327,6 +410,7 @@ jobs:
|
|||||||
body: |
|
body: |
|
||||||
Release assets are published for all supported build targets:
|
Release assets are published for all supported build targets:
|
||||||
- Android (`.apk`)
|
- Android (`.apk`)
|
||||||
|
- Google Play (`.aab`)
|
||||||
- Linux (`.tar.gz`)
|
- Linux (`.tar.gz`)
|
||||||
- macOS (`.dmg`)
|
- macOS (`.dmg`)
|
||||||
- Windows (`.zip`)
|
- Windows (`.zip`)
|
||||||
@@ -341,6 +425,7 @@ jobs:
|
|||||||
4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators).
|
4. Install the signed `.ipa` on your iPhone using Apple Configurator 2 or Xcode (Devices and Simulators).
|
||||||
files: |
|
files: |
|
||||||
dist/*.apk
|
dist/*.apk
|
||||||
|
dist/*.aab
|
||||||
dist/*.tar.gz
|
dist/*.tar.gz
|
||||||
dist/*.zip
|
dist/*.zip
|
||||||
dist/*.dmg
|
dist/*.dmg
|
||||||
|
|||||||
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
|
||||||
|
|||||||
9
.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/
|
||||||
@@ -72,15 +73,23 @@ GoogleService-Info.plist
|
|||||||
**/fastlane/.env
|
**/fastlane/.env
|
||||||
**/fastlane/.env.*
|
**/fastlane/.env.*
|
||||||
fastlane/README.md
|
fastlane/README.md
|
||||||
|
ios/.bundle/
|
||||||
|
ios/vendor/
|
||||||
|
|
||||||
# Play Store release assets & scripts
|
# Play Store release assets & scripts
|
||||||
play-store/
|
play-store/
|
||||||
create_feature_graphic.py
|
create_feature_graphic.py
|
||||||
|
|
||||||
# Tool caches and local state
|
# Tool caches and local state
|
||||||
|
.devenv/
|
||||||
.cachebro/
|
.cachebro/
|
||||||
.osgrep/
|
.osgrep/
|
||||||
third_party/lpcnet_flutter/
|
third_party/lpcnet_flutter/
|
||||||
|
worker/node_modules/
|
||||||
|
worker/.wrangler/
|
||||||
|
worker/.bun/
|
||||||
|
worker/.astro/
|
||||||
|
worker/dist/
|
||||||
|
|
||||||
# Claude Code local settings (permissions, personal config)
|
# Claude Code local settings (permissions, personal config)
|
||||||
.claude/settings.local.json
|
.claude/settings.local.json
|
||||||
|
|||||||
12
CHANGELOG.md
@@ -1,5 +1,17 @@
|
|||||||
# Changelog
|
# 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
|
## 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.
|
- Discovery and contacts: finished the discovery flow, removed artificial UI delays, added profile storage and sync, and allowed contact name overrides.
|
||||||
|
|||||||
3
Makefile
@@ -135,6 +135,9 @@ clean: ## Clean build artifacts
|
|||||||
flutter clean
|
flutter clean
|
||||||
rm -rf $(BUILD_DIR)
|
rm -rf $(BUILD_DIR)
|
||||||
|
|
||||||
|
build-web: ## Build web with WebAssembly
|
||||||
|
flutter build web --wasm --release
|
||||||
|
|
||||||
# Build for all platforms
|
# Build for all platforms
|
||||||
build-all: bump ## Build for Android and iOS
|
build-all: bump ## Build for Android and iOS
|
||||||
flutter build apk --release
|
flutter build apk --release
|
||||||
|
|||||||
20
README.md
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
|
MeshCore SAR helps teams coordinate in low-connectivity or no-connectivity environments with messaging, voice, images, maps, and live location context in one app.
|
||||||
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
|
It uses the MeshCore protocol over LoRa for long-range, infrastructure-free communication.
|
||||||
`iOS TestFlight:` https://testflight.apple.com/join/nCZYMPPz
|
`iOS TestFlight:` https://testflight.apple.com/join/tngpPF12
|
||||||
|
|
||||||
## Highlights
|
## Highlights
|
||||||
|
|
||||||
@@ -75,6 +75,24 @@ It uses the MeshCore protocol over LoRa for long-range, infrastructure-free comm
|
|||||||
- Incident command and coordination roles
|
- Incident command and coordination roles
|
||||||
- Operators working in weak/no cellular coverage
|
- Operators working in weak/no cellular coverage
|
||||||
|
|
||||||
|
## Connecting Your Radio (BLE)
|
||||||
|
|
||||||
|
The app pairs with your MeshCore radio over Bluetooth Low Energy.
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
On Windows 11 (tested on 25H2), pair the radio through the system Bluetooth menu **before** connecting in the app:
|
||||||
|
|
||||||
|
1. Open **Settings → Bluetooth & devices → Add device → Bluetooth**.
|
||||||
|
2. Select your MeshCore radio and complete pairing, entering the **PIN** when prompted.
|
||||||
|
3. Launch MeshCore SAR and connect — the radio now appears as a paired device.
|
||||||
|
|
||||||
|
If the radio does not show up or fails to connect in-app, remove it from the Windows Bluetooth menu and repeat the pairing step.
|
||||||
|
|
||||||
|
### iOS / Android
|
||||||
|
|
||||||
|
Connect directly from within the app — no separate system pairing step is required.
|
||||||
|
|
||||||
## Permissions (App Use)
|
## Permissions (App Use)
|
||||||
|
|
||||||
- Bluetooth: mesh device communication
|
- Bluetooth: mesh device communication
|
||||||
|
|||||||
4
android/Gemfile
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "fastlane"
|
||||||
|
gem "ostruct"
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
<!-- Bluetooth permissions -->
|
<!-- Bluetooth permissions -->
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH" android:maxSdkVersion="30" />
|
||||||
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" android:maxSdkVersion="30" />
|
||||||
@@ -62,6 +63,12 @@
|
|||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
|
<service
|
||||||
|
android:name="id.flutter.flutter_background_service.BackgroundService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="location"
|
||||||
|
android:stopWithTask="false"
|
||||||
|
tools:replace="android:exported" />
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
|
|||||||
@@ -16,6 +16,24 @@ subprojects {
|
|||||||
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
}
|
}
|
||||||
subprojects {
|
subprojects {
|
||||||
|
// flutter_avif_android 3.1.0 (latest) ships duplicate Java and Kotlin
|
||||||
|
// copies of FlutterAvifPlugin. Kotlin 2.2+ rejects the redeclaration. The
|
||||||
|
// plugin's Java source dir contains only this redundant copy, so empty it
|
||||||
|
// and let the identical Kotlin implementation compile. kotlinc reads the
|
||||||
|
// module's Java source roots for joint compilation, so clearing srcDirs
|
||||||
|
// (rather than an exclude filter, which AGP doesn't forward to kotlinc) is
|
||||||
|
// what actually removes the duplicate from its view. Registered before
|
||||||
|
// evaluationDependsOn so it also covers :app, which that call evaluates
|
||||||
|
// eagerly.
|
||||||
|
afterEvaluate {
|
||||||
|
if (project.name == "flutter_avif_android") {
|
||||||
|
extensions.findByName("android")?.let { ext ->
|
||||||
|
(ext as com.android.build.gradle.BaseExtension)
|
||||||
|
.sourceSets.getByName("main").java
|
||||||
|
.setSrcDirs(emptyList<String>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,44 +16,72 @@
|
|||||||
default_platform(:android)
|
default_platform(:android)
|
||||||
|
|
||||||
platform :android do
|
platform :android do
|
||||||
|
def project_root
|
||||||
|
File.expand_path("../..", __dir__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def play_service_account_json
|
||||||
|
ENV.fetch("GOOGLE_PLAY_SERVICE_ACCOUNT_JSON")
|
||||||
|
end
|
||||||
|
|
||||||
|
def play_build_command
|
||||||
|
"cd #{project_root} && flutter build appbundle --release"
|
||||||
|
end
|
||||||
|
|
||||||
|
def play_aab_path
|
||||||
|
File.join(project_root, "build/app/outputs/bundle/release/app-release.aab")
|
||||||
|
end
|
||||||
|
|
||||||
|
def play_package_name
|
||||||
|
"com.meshcore.sar.meshcore_sar_app"
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_apk_build_command
|
||||||
|
"cd #{project_root} && flutter build apk --release"
|
||||||
|
end
|
||||||
|
|
||||||
|
def direct_apk_path
|
||||||
|
File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk")
|
||||||
|
end
|
||||||
|
|
||||||
|
def skip_android_build?
|
||||||
|
ENV["SKIP_ANDROID_BUILD"] == "1"
|
||||||
|
end
|
||||||
|
|
||||||
desc "Runs all the tests"
|
desc "Runs all the tests"
|
||||||
lane :test do
|
lane :test do
|
||||||
gradle(task: "test")
|
gradle(task: "test")
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Build release APK"
|
desc "Build release APK"
|
||||||
lane :beta do
|
lane :direct_apk do
|
||||||
# Get the project root directory (two levels up from android/fastlane/)
|
sh(direct_apk_build_command)
|
||||||
project_root = File.expand_path("../..", __dir__)
|
|
||||||
|
|
||||||
# Build APK with Flutter
|
UI.success("APK built successfully at: #{direct_apk_path}")
|
||||||
sh("cd #{project_root} && flutter build apk --release")
|
|
||||||
|
|
||||||
apk_path = File.join(project_root, "build/app/outputs/flutter-apk/app-release.apk")
|
|
||||||
|
|
||||||
# Uncomment to distribute via Firebase App Distribution:
|
|
||||||
# firebase_app_distribution(
|
|
||||||
# app: "YOUR_FIREBASE_APP_ID",
|
|
||||||
# apk_path: apk_path,
|
|
||||||
# groups: "testers"
|
|
||||||
# )
|
|
||||||
|
|
||||||
UI.success("APK built at: #{apk_path}")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Deploy a new version to the Google Play"
|
desc "Build Play release AAB and upload to internal testing"
|
||||||
lane :deploy do
|
lane :internal do
|
||||||
# Get the project root directory (two levels up from android/fastlane/)
|
sh(play_build_command) unless skip_android_build?
|
||||||
project_root = File.expand_path("../..", __dir__)
|
|
||||||
|
|
||||||
# Build AAB with Flutter (required for Play Store since 2021)
|
|
||||||
sh("cd #{project_root} && flutter build appbundle --release")
|
|
||||||
|
|
||||||
aab_path = File.join(project_root, "build/app/outputs/bundle/release/app-release.aab")
|
|
||||||
|
|
||||||
upload_to_play_store(
|
upload_to_play_store(
|
||||||
aab: aab_path,
|
aab: play_aab_path,
|
||||||
track: "internal"
|
package_name: play_package_name,
|
||||||
|
track: "internal",
|
||||||
|
release_status: "draft",
|
||||||
|
json_key_data: play_service_account_json
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Build Play release AAB and upload to production"
|
||||||
|
lane :production do
|
||||||
|
sh(play_build_command) unless skip_android_build?
|
||||||
|
|
||||||
|
upload_to_play_store(
|
||||||
|
aab: play_aab_path,
|
||||||
|
package_name: play_package_name,
|
||||||
|
track: "production",
|
||||||
|
json_key_data: play_service_account_json
|
||||||
)
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,3 +1,13 @@
|
|||||||
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
# Kotlin 2.2+ fails the build when a module's Java and Kotlin compile tasks
|
||||||
|
# target different JVM versions. Several Flutter plugins still pin mismatched
|
||||||
|
# targets (e.g. Java 1.8 vs Kotlin 1.8, or Java 11 vs Kotlin 17). On Android
|
||||||
|
# every class is dexed by D8/R8, so the class-file version mismatch is benign;
|
||||||
|
# downgrade the strict check to a warning so third-party plugins build.
|
||||||
|
kotlin.jvm.target.validation.mode=warning
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
|
|||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
zipStorePath=wrapper/dists
|
zipStorePath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ pluginManagement {
|
|||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
|
||||||
id("com.android.application") version "8.9.1" apply false
|
id("com.android.application") version "8.11.1" apply false
|
||||||
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
id("org.jetbrains.kotlin.android") version "2.2.20" apply false
|
||||||
}
|
}
|
||||||
|
|
||||||
include(":app")
|
include(":app")
|
||||||
|
|||||||
128
docs/google-play-release.md
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
# Google Play Release Runbook
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This runbook prepares and ships the Android release build for `com.meshcore.sar.meshcore_sar_app`.
|
||||||
|
|
||||||
|
## Distribution Modes
|
||||||
|
|
||||||
|
- GitHub release: signed APK for direct install or manual distribution.
|
||||||
|
- Google Play: signed AAB uploaded through Fastlane to Play internal testing and, for non-prerelease GitHub releases, production.
|
||||||
|
|
||||||
|
## Required Secrets and Local Files
|
||||||
|
|
||||||
|
GitHub Actions secrets for `dz0ny/meshcore-sar`:
|
||||||
|
|
||||||
|
- `ANDROID_KEYSTORE_BASE64`
|
||||||
|
- `ANDROID_KEYSTORE_PASSWORD`
|
||||||
|
- `ANDROID_KEY_ALIAS`
|
||||||
|
- `ANDROID_KEY_PASSWORD`
|
||||||
|
- `GOOGLE_PLAY_SERVICE_ACCOUNT_JSON`
|
||||||
|
|
||||||
|
Local files used to seed the secrets:
|
||||||
|
|
||||||
|
- `android/key.properties`
|
||||||
|
- release keystore referenced by `android/key.properties`
|
||||||
|
- `/Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json`
|
||||||
|
|
||||||
|
The Play Console service account must have release access for `com.meshcore.sar.meshcore_sar_app`.
|
||||||
|
|
||||||
|
## CI Release Flow
|
||||||
|
|
||||||
|
The release flow runs from `.github/workflows/build-artifacts.yml` when a GitHub release is published.
|
||||||
|
|
||||||
|
Android release steps:
|
||||||
|
|
||||||
|
1. Validate all Android signing and Google Play secrets.
|
||||||
|
2. Recreate `android/release-keystore.jks` and `android/key.properties` from GitHub secrets.
|
||||||
|
3. Build the signed APK with `flutter build apk --release`.
|
||||||
|
4. Build the signed Play AAB with `flutter build appbundle --release`.
|
||||||
|
5. Upload the APK and AAB as GitHub release assets.
|
||||||
|
6. Upload the AAB to Play internal testing with `bundle exec fastlane android internal`.
|
||||||
|
7. Upload the AAB to Play production when the GitHub release is not marked as prerelease.
|
||||||
|
|
||||||
|
The same workflow also builds Linux, macOS, Windows, iOS unsigned, and web artifacts.
|
||||||
|
|
||||||
|
## Versioning Rules
|
||||||
|
|
||||||
|
- Version source of truth: `pubspec.yaml`.
|
||||||
|
- Android `versionCode` is the number after `+`.
|
||||||
|
- `versionCode` must always increase for Play uploads.
|
||||||
|
- `make bump`, `make build`, and `make bundle` increment the version.
|
||||||
|
- Use `make build-no-bump` or `make bundle-no-bump` only when intentionally rebuilding the same version locally.
|
||||||
|
|
||||||
|
## Manual Build Commands
|
||||||
|
|
||||||
|
Run commands from the repo root.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
flutter build apk --release
|
||||||
|
flutter build appbundle --release
|
||||||
|
```
|
||||||
|
|
||||||
|
Repo shortcuts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
make bundle
|
||||||
|
make build-no-bump
|
||||||
|
make bundle-no-bump
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fastlane Lanes
|
||||||
|
|
||||||
|
Run commands from `android/`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec fastlane android direct_apk
|
||||||
|
bundle exec fastlane android internal
|
||||||
|
bundle exec fastlane android production
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `SKIP_ANDROID_BUILD=1` when an AAB has already been built and Fastlane should only upload it.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKIP_ANDROID_BUILD=1 GOOGLE_PLAY_SERVICE_ACCOUNT_JSON="$(cat /Users/dz0ny/android-keystores/fastlane-480919-0cb30c62db50.json)" bundle exec fastlane android internal
|
||||||
|
```
|
||||||
|
|
||||||
|
## Expected Artifacts
|
||||||
|
|
||||||
|
- APK: `build/app/outputs/flutter-apk/app-release.apk`
|
||||||
|
- AAB: `build/app/outputs/bundle/release/app-release.aab`
|
||||||
|
- Release APK asset: `meshcore-sar-<tag>-android.apk`
|
||||||
|
- Release AAB asset: `meshcore-sar-<tag>-play.aab`
|
||||||
|
|
||||||
|
## Internal Testing Release Checklist
|
||||||
|
|
||||||
|
1. Confirm `pubspec.yaml` version is correct and `versionCode` increased.
|
||||||
|
2. Confirm `android/key.properties` points to the release keystore for local builds.
|
||||||
|
3. Confirm all five GitHub Actions secrets exist.
|
||||||
|
4. Publish a prerelease in GitHub to build assets and upload Play internal testing without production promotion.
|
||||||
|
5. Install from the internal testing track on a real Android device.
|
||||||
|
6. Smoke test startup, permissions, map, messaging, telemetry, and offline map behavior.
|
||||||
|
|
||||||
|
## Production Release Checklist
|
||||||
|
|
||||||
|
1. Complete internal testing validation first.
|
||||||
|
2. Confirm Play Console forms are current:
|
||||||
|
- App content
|
||||||
|
- Data safety
|
||||||
|
- App access
|
||||||
|
- Ads declaration
|
||||||
|
- Content rating
|
||||||
|
3. Confirm store assets are current:
|
||||||
|
- app icon
|
||||||
|
- feature graphic
|
||||||
|
- phone screenshots
|
||||||
|
- tablet screenshots if used
|
||||||
|
- support URL
|
||||||
|
- privacy policy URL
|
||||||
|
4. Publish a non-prerelease GitHub release.
|
||||||
|
5. Confirm the GitHub release has the APK and AAB assets.
|
||||||
|
6. Confirm Play Console has the new internal and production release entries.
|
||||||
|
|
||||||
|
## Rollback Rules
|
||||||
|
|
||||||
|
- Never reuse or lower an Android `versionCode`.
|
||||||
|
- If a Play release is bad, halt rollout in Play Console and ship a new higher-version fix.
|
||||||
|
- Keep GitHub direct APK and Play AAB artifacts separate; do not upload APKs to Play.
|
||||||
116
integration_test/app_screenshots_test.dart
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:integration_test/integration_test.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
import 'package:meshcore_sar_app/l10n/app_localizations.dart';
|
||||||
|
import 'package:meshcore_sar_app/main.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/contacts_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/providers/messages_provider.dart';
|
||||||
|
import 'package:meshcore_sar_app/screens/home_screen.dart';
|
||||||
|
import 'package:meshcore_sar_app/services/wizard_preferences.dart';
|
||||||
|
import 'package:meshcore_sar_app/utils/sample_data_generator.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
const screenshotPrefix = String.fromEnvironment('SCREENSHOT_PREFIX');
|
||||||
|
|
||||||
|
testWidgets('captures App Store screenshots', (tester) async {
|
||||||
|
SharedPreferences.setMockInitialValues({
|
||||||
|
'wizard_completed': true,
|
||||||
|
'wizard_version': 1,
|
||||||
|
});
|
||||||
|
await WizardPreferences.setWizardCompleted(true);
|
||||||
|
|
||||||
|
await tester.pumpWidget(const MeshCoreSarApp());
|
||||||
|
await _pumpUntilFound(tester, find.byType(HomeScreen));
|
||||||
|
await binding.convertFlutterSurfaceToImage();
|
||||||
|
|
||||||
|
final homeContext = tester.element(find.byType(HomeScreen));
|
||||||
|
_loadSampleData(homeContext);
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}01-messages');
|
||||||
|
|
||||||
|
await _tapTab(tester, 'Contacts');
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}02-contacts');
|
||||||
|
|
||||||
|
await _tapTab(tester, 'Map');
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}03-map');
|
||||||
|
|
||||||
|
await _openOverflowItem(tester, Icons.more_vert, 'Settings');
|
||||||
|
await _takeScreenshot(binding, tester, '${screenshotPrefix}04-settings');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _loadSampleData(BuildContext context) {
|
||||||
|
final l10n = AppLocalizations.of(context)!;
|
||||||
|
const centerLocation = LatLng(46.0569, 14.5058);
|
||||||
|
final contacts = SampleDataGenerator.generateContacts(
|
||||||
|
centerLocation: centerLocation,
|
||||||
|
l10n: l10n,
|
||||||
|
teamMemberCount: 5,
|
||||||
|
channelCount: 2,
|
||||||
|
);
|
||||||
|
final sampleMessages = SampleDataGenerator.generateAllMessages(
|
||||||
|
centerLocation: centerLocation,
|
||||||
|
l10n: l10n,
|
||||||
|
foundPersonCount: 2,
|
||||||
|
fireCount: 1,
|
||||||
|
stagingCount: 1,
|
||||||
|
objectCount: 1,
|
||||||
|
generalChannelMessages: 8,
|
||||||
|
emergencyChannelMessages: 5,
|
||||||
|
);
|
||||||
|
context.read<ContactsProvider>().addContacts(contacts);
|
||||||
|
for (final message in sampleMessages.messages) {
|
||||||
|
context.read<MessagesProvider>().addMessage(
|
||||||
|
message,
|
||||||
|
contactLocationSnapshot: sampleMessages.contactLocations[message.id],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpUntilFound(
|
||||||
|
WidgetTester tester,
|
||||||
|
Finder finder, {
|
||||||
|
Duration timeout = const Duration(seconds: 10),
|
||||||
|
}) async {
|
||||||
|
final end = DateTime.now().add(timeout);
|
||||||
|
while (DateTime.now().isBefore(end)) {
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
if (finder.evaluate().isNotEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(finder, findsOneWidget);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _tapTab(WidgetTester tester, String label) async {
|
||||||
|
final tab = find.text(label).last;
|
||||||
|
await tester.tap(tab);
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _openOverflowItem(
|
||||||
|
WidgetTester tester,
|
||||||
|
IconData buttonIcon,
|
||||||
|
String label,
|
||||||
|
) async {
|
||||||
|
await tester.tap(find.byIcon(buttonIcon));
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
await tester.tap(find.text(label));
|
||||||
|
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _takeScreenshot(
|
||||||
|
IntegrationTestWidgetsFlutterBinding binding,
|
||||||
|
WidgetTester tester,
|
||||||
|
String name,
|
||||||
|
) async {
|
||||||
|
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||||
|
final bytes = await binding.takeScreenshot(name);
|
||||||
|
expect(bytes.isNotEmpty, isTrue);
|
||||||
|
}
|
||||||
@@ -1,49 +1,47 @@
|
|||||||
GEM
|
GEM
|
||||||
remote: https://rubygems.org/
|
remote: https://rubygems.org/
|
||||||
specs:
|
specs:
|
||||||
CFPropertyList (3.0.7)
|
CFPropertyList (3.0.9)
|
||||||
base64
|
abbrev (0.1.2)
|
||||||
nkf
|
addressable (2.9.0)
|
||||||
rexml
|
public_suffix (>= 2.0.2, < 8.0)
|
||||||
addressable (2.8.7)
|
|
||||||
public_suffix (>= 2.0.2, < 7.0)
|
|
||||||
artifactory (3.0.17)
|
artifactory (3.0.17)
|
||||||
atomos (0.1.3)
|
atomos (0.1.3)
|
||||||
aws-eventstream (1.4.0)
|
aws-eventstream (1.3.2)
|
||||||
aws-partitions (1.1172.0)
|
aws-partitions (1.1109.0)
|
||||||
aws-sdk-core (3.233.0)
|
aws-sdk-core (3.224.1)
|
||||||
aws-eventstream (~> 1, >= 1.3.0)
|
aws-eventstream (~> 1, >= 1.3.0)
|
||||||
aws-partitions (~> 1, >= 1.992.0)
|
aws-partitions (~> 1, >= 1.992.0)
|
||||||
aws-sigv4 (~> 1.9)
|
aws-sigv4 (~> 1.9)
|
||||||
base64
|
base64
|
||||||
bigdecimal
|
|
||||||
jmespath (~> 1, >= 1.6.1)
|
jmespath (~> 1, >= 1.6.1)
|
||||||
logger
|
logger
|
||||||
aws-sdk-kms (1.113.0)
|
aws-sdk-kms (1.101.0)
|
||||||
aws-sdk-core (~> 3, >= 3.231.0)
|
aws-sdk-core (~> 3, >= 3.216.0)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sdk-s3 (1.199.1)
|
aws-sdk-s3 (1.188.0)
|
||||||
aws-sdk-core (~> 3, >= 3.231.0)
|
aws-sdk-core (~> 3, >= 3.224.1)
|
||||||
aws-sdk-kms (~> 1)
|
aws-sdk-kms (~> 1)
|
||||||
aws-sigv4 (~> 1.5)
|
aws-sigv4 (~> 1.5)
|
||||||
aws-sigv4 (1.12.1)
|
aws-sigv4 (1.11.0)
|
||||||
aws-eventstream (~> 1, >= 1.0.2)
|
aws-eventstream (~> 1, >= 1.0.2)
|
||||||
babosa (1.0.4)
|
babosa (1.0.4)
|
||||||
base64 (0.3.0)
|
base64 (0.3.0)
|
||||||
bigdecimal (3.3.1)
|
|
||||||
claide (1.1.0)
|
claide (1.1.0)
|
||||||
colored (1.2)
|
colored (1.2)
|
||||||
colored2 (3.1.2)
|
colored2 (3.1.2)
|
||||||
commander (4.6.0)
|
commander (4.6.0)
|
||||||
highline (~> 2.0.0)
|
highline (~> 2.0.0)
|
||||||
|
csv (3.3.5)
|
||||||
declarative (0.0.20)
|
declarative (0.0.20)
|
||||||
digest-crc (0.7.0)
|
digest-crc (0.7.0)
|
||||||
rake (>= 12.0.0, < 14.0.0)
|
rake (>= 12.0.0, < 14.0.0)
|
||||||
domain_name (0.6.20240107)
|
domain_name (0.5.20190701)
|
||||||
|
unf (>= 0.0.5, < 1.0.0)
|
||||||
dotenv (2.8.1)
|
dotenv (2.8.1)
|
||||||
emoji_regex (3.2.3)
|
emoji_regex (3.2.3)
|
||||||
excon (0.112.0)
|
excon (0.109.0)
|
||||||
faraday (1.10.4)
|
faraday (1.10.5)
|
||||||
faraday-em_http (~> 1.0)
|
faraday-em_http (~> 1.0)
|
||||||
faraday-em_synchrony (~> 1.0)
|
faraday-em_synchrony (~> 1.0)
|
||||||
faraday-excon (~> 1.1)
|
faraday-excon (~> 1.1)
|
||||||
@@ -55,25 +53,26 @@ GEM
|
|||||||
faraday-rack (~> 1.0)
|
faraday-rack (~> 1.0)
|
||||||
faraday-retry (~> 1.0)
|
faraday-retry (~> 1.0)
|
||||||
ruby2_keywords (>= 0.0.4)
|
ruby2_keywords (>= 0.0.4)
|
||||||
faraday-cookie_jar (0.0.7)
|
faraday-cookie_jar (0.0.8)
|
||||||
faraday (>= 0.8.0)
|
faraday (>= 0.8.0)
|
||||||
http-cookie (~> 1.0.0)
|
http-cookie (>= 1.0.0)
|
||||||
faraday-em_http (1.0.0)
|
faraday-em_http (1.0.0)
|
||||||
faraday-em_synchrony (1.0.1)
|
faraday-em_synchrony (1.0.1)
|
||||||
faraday-excon (1.1.0)
|
faraday-excon (1.1.0)
|
||||||
faraday-httpclient (1.0.1)
|
faraday-httpclient (1.0.1)
|
||||||
faraday-multipart (1.1.1)
|
faraday-multipart (1.2.0)
|
||||||
multipart-post (~> 2.0)
|
multipart-post (~> 2.0)
|
||||||
faraday-net_http (1.0.2)
|
faraday-net_http (1.0.2)
|
||||||
faraday-net_http_persistent (1.2.0)
|
faraday-net_http_persistent (1.2.0)
|
||||||
faraday-patron (1.0.0)
|
faraday-patron (1.0.0)
|
||||||
faraday-rack (1.0.0)
|
faraday-rack (1.0.0)
|
||||||
faraday-retry (1.0.3)
|
faraday-retry (1.0.4)
|
||||||
faraday_middleware (1.2.1)
|
faraday_middleware (1.2.1)
|
||||||
faraday (~> 1.0)
|
faraday (~> 1.0)
|
||||||
fastimage (2.4.0)
|
fastimage (2.4.1)
|
||||||
fastlane (2.228.0)
|
fastlane (2.229.0)
|
||||||
CFPropertyList (>= 2.3, < 4.0.0)
|
CFPropertyList (>= 2.3, < 4.0.0)
|
||||||
|
abbrev (~> 0.1.2)
|
||||||
addressable (>= 2.8, < 3.0.0)
|
addressable (>= 2.8, < 3.0.0)
|
||||||
artifactory (~> 3.0)
|
artifactory (~> 3.0)
|
||||||
aws-sdk-s3 (~> 1.0)
|
aws-sdk-s3 (~> 1.0)
|
||||||
@@ -81,6 +80,7 @@ GEM
|
|||||||
bundler (>= 1.12.0, < 3.0.0)
|
bundler (>= 1.12.0, < 3.0.0)
|
||||||
colored (~> 1.2)
|
colored (~> 1.2)
|
||||||
commander (~> 4.6)
|
commander (~> 4.6)
|
||||||
|
csv (~> 3.3)
|
||||||
dotenv (>= 2.1.1, < 3.0.0)
|
dotenv (>= 2.1.1, < 3.0.0)
|
||||||
emoji_regex (>= 0.1, < 4.0)
|
emoji_regex (>= 0.1, < 4.0)
|
||||||
excon (>= 0.71.0, < 1.0.0)
|
excon (>= 0.71.0, < 1.0.0)
|
||||||
@@ -100,6 +100,7 @@ GEM
|
|||||||
jwt (>= 2.1.0, < 3)
|
jwt (>= 2.1.0, < 3)
|
||||||
mini_magick (>= 4.9.4, < 5.0.0)
|
mini_magick (>= 4.9.4, < 5.0.0)
|
||||||
multipart-post (>= 2.0.0, < 3.0.0)
|
multipart-post (>= 2.0.0, < 3.0.0)
|
||||||
|
mutex_m (~> 0.3.0)
|
||||||
naturally (~> 2.2)
|
naturally (~> 2.2)
|
||||||
optparse (>= 0.1.1, < 1.0.0)
|
optparse (>= 0.1.1, < 1.0.0)
|
||||||
plist (>= 3.1.0, < 4.0.0)
|
plist (>= 3.1.0, < 4.0.0)
|
||||||
@@ -114,8 +115,7 @@ GEM
|
|||||||
xcodeproj (>= 1.13.0, < 2.0.0)
|
xcodeproj (>= 1.13.0, < 2.0.0)
|
||||||
xcpretty (~> 0.4.1)
|
xcpretty (~> 0.4.1)
|
||||||
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
xcpretty-travis-formatter (>= 0.0.3, < 2.0.0)
|
||||||
fastlane-sirp (1.0.0)
|
fastlane-sirp (1.1.0)
|
||||||
sysrandom (~> 1.0)
|
|
||||||
gh_inspector (1.1.3)
|
gh_inspector (1.1.3)
|
||||||
google-apis-androidpublisher_v3 (0.54.0)
|
google-apis-androidpublisher_v3 (0.54.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
@@ -131,19 +131,19 @@ GEM
|
|||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-playcustomapp_v1 (0.13.0)
|
google-apis-playcustomapp_v1 (0.13.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-apis-storage_v1 (0.31.0)
|
google-apis-storage_v1 (0.32.0)
|
||||||
google-apis-core (>= 0.11.0, < 2.a)
|
google-apis-core (>= 0.11.0, < 2.a)
|
||||||
google-cloud-core (1.8.0)
|
google-cloud-core (1.6.1)
|
||||||
google-cloud-env (>= 1.0, < 3.a)
|
google-cloud-env (>= 1.0, < 3.a)
|
||||||
google-cloud-errors (~> 1.0)
|
google-cloud-errors (~> 1.0)
|
||||||
google-cloud-env (1.6.0)
|
google-cloud-env (1.6.0)
|
||||||
faraday (>= 0.17.3, < 3.0)
|
faraday (>= 0.17.3, < 3.0)
|
||||||
google-cloud-errors (1.5.0)
|
google-cloud-errors (1.3.1)
|
||||||
google-cloud-storage (1.47.0)
|
google-cloud-storage (1.37.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
digest-crc (~> 0.4)
|
digest-crc (~> 0.4)
|
||||||
google-apis-iamcredentials_v1 (~> 0.1)
|
google-apis-iamcredentials_v1 (~> 0.1)
|
||||||
google-apis-storage_v1 (~> 0.31.0)
|
google-apis-storage_v1 (~> 0.1)
|
||||||
google-cloud-core (~> 1.6)
|
google-cloud-core (~> 1.6)
|
||||||
googleauth (>= 0.16.2, < 2.a)
|
googleauth (>= 0.16.2, < 2.a)
|
||||||
mini_mime (~> 1.0)
|
mini_mime (~> 1.0)
|
||||||
@@ -159,42 +159,40 @@ GEM
|
|||||||
httpclient (2.9.0)
|
httpclient (2.9.0)
|
||||||
mutex_m
|
mutex_m
|
||||||
jmespath (1.6.2)
|
jmespath (1.6.2)
|
||||||
json (2.15.1)
|
json (2.7.6)
|
||||||
jwt (2.10.2)
|
jwt (2.10.3)
|
||||||
base64
|
base64
|
||||||
logger (1.7.0)
|
logger (1.7.0)
|
||||||
mini_magick (4.13.2)
|
mini_magick (4.13.2)
|
||||||
mini_mime (1.1.5)
|
mini_mime (1.1.5)
|
||||||
multi_json (1.17.0)
|
multi_json (1.15.0)
|
||||||
multipart-post (2.4.1)
|
multipart-post (2.4.1)
|
||||||
mutex_m (0.3.0)
|
mutex_m (0.3.0)
|
||||||
nanaimo (0.4.0)
|
nanaimo (0.4.0)
|
||||||
naturally (2.3.0)
|
naturally (2.3.0)
|
||||||
nkf (0.2.0)
|
optparse (0.8.1)
|
||||||
optparse (0.6.0)
|
|
||||||
os (1.1.4)
|
os (1.1.4)
|
||||||
plist (3.7.2)
|
plist (3.7.2)
|
||||||
public_suffix (6.0.2)
|
public_suffix (5.1.1)
|
||||||
rake (13.3.0)
|
rake (13.4.2)
|
||||||
representable (3.2.0)
|
representable (3.2.0)
|
||||||
declarative (< 0.1.0)
|
declarative (< 0.1.0)
|
||||||
trailblazer-option (>= 0.1.1, < 0.2.0)
|
trailblazer-option (>= 0.1.1, < 0.2.0)
|
||||||
uber (< 0.2.0)
|
uber (< 0.2.0)
|
||||||
retriable (3.1.2)
|
retriable (3.8.0)
|
||||||
rexml (3.4.4)
|
rexml (3.4.4)
|
||||||
rouge (3.28.0)
|
rouge (3.28.0)
|
||||||
ruby2_keywords (0.0.5)
|
ruby2_keywords (0.0.5)
|
||||||
rubyzip (2.4.1)
|
rubyzip (2.4.1)
|
||||||
security (0.1.5)
|
security (0.1.5)
|
||||||
signet (0.21.0)
|
signet (0.18.0)
|
||||||
addressable (~> 2.8)
|
addressable (~> 2.8)
|
||||||
faraday (>= 0.17.5, < 3.a)
|
faraday (>= 0.17.5, < 3.a)
|
||||||
jwt (>= 1.5, < 4.0)
|
jwt (>= 1.5, < 3.0)
|
||||||
multi_json (~> 1.10)
|
multi_json (~> 1.10)
|
||||||
simctl (1.6.10)
|
simctl (1.6.10)
|
||||||
CFPropertyList
|
CFPropertyList
|
||||||
naturally
|
naturally
|
||||||
sysrandom (1.0.5)
|
|
||||||
terminal-notifier (2.0.0)
|
terminal-notifier (2.0.0)
|
||||||
terminal-table (3.0.2)
|
terminal-table (3.0.2)
|
||||||
unicode-display_width (>= 1.1.1, < 3)
|
unicode-display_width (>= 1.1.1, < 3)
|
||||||
@@ -204,6 +202,7 @@ GEM
|
|||||||
tty-spinner (0.9.3)
|
tty-spinner (0.9.3)
|
||||||
tty-cursor (~> 0.7)
|
tty-cursor (~> 0.7)
|
||||||
uber (0.1.0)
|
uber (0.1.0)
|
||||||
|
unf (0.2.0)
|
||||||
unicode-display_width (2.6.0)
|
unicode-display_width (2.6.0)
|
||||||
word_wrap (1.0.0)
|
word_wrap (1.0.0)
|
||||||
xcodeproj (1.27.0)
|
xcodeproj (1.27.0)
|
||||||
@@ -226,4 +225,4 @@ DEPENDENCIES
|
|||||||
fastlane
|
fastlane
|
||||||
|
|
||||||
BUNDLED WITH
|
BUNDLED WITH
|
||||||
2.7.2
|
2.4.22
|
||||||
|
|||||||
143
ios/Podfile.lock
@@ -1,194 +1,51 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- audioplayers_darwin (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- codec2_flutter (0.1.0):
|
- codec2_flutter (0.1.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- device_info_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- DKImagePickerController/Core (4.3.9):
|
|
||||||
- DKImagePickerController/ImageDataManager
|
|
||||||
- DKImagePickerController/Resource
|
|
||||||
- DKImagePickerController/ImageDataManager (4.3.9)
|
|
||||||
- DKImagePickerController/PhotoGallery (4.3.9):
|
|
||||||
- DKImagePickerController/Core
|
|
||||||
- DKPhotoGallery
|
|
||||||
- DKImagePickerController/Resource (4.3.9)
|
|
||||||
- DKPhotoGallery (0.0.19):
|
|
||||||
- DKPhotoGallery/Core (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Model (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Preview (= 0.0.19)
|
|
||||||
- DKPhotoGallery/Resource (= 0.0.19)
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Core (0.0.19):
|
|
||||||
- DKPhotoGallery/Model
|
|
||||||
- DKPhotoGallery/Preview
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Model (0.0.19):
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Preview (0.0.19):
|
|
||||||
- DKPhotoGallery/Model
|
|
||||||
- DKPhotoGallery/Resource
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- DKPhotoGallery/Resource (0.0.19):
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
- file_picker (0.0.1):
|
|
||||||
- DKImagePickerController/PhotoGallery
|
|
||||||
- Flutter
|
|
||||||
- Flutter (1.0.0)
|
- Flutter (1.0.0)
|
||||||
- flutter_avif_ios (0.0.1):
|
- flutter_avif_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_background_service_ios (0.0.3):
|
- flutter_background_service_ios (0.0.3):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_blue_plus_darwin (0.0.2):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- flutter_compass (0.0.1):
|
- flutter_compass (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- flutter_local_notifications (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- geolocator_apple (1.2.0):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- image_picker_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- nsd_ios (0.0.1):
|
- nsd_ios (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- package_info_plus (0.4.5):
|
|
||||||
- Flutter
|
|
||||||
- path_provider_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- permission_handler_apple (9.3.0):
|
- permission_handler_apple (9.3.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- record_ios (1.2.0):
|
|
||||||
- Flutter
|
|
||||||
- SDWebImage (5.21.7):
|
|
||||||
- SDWebImage/Core (= 5.21.7)
|
|
||||||
- SDWebImage/Core (5.21.7)
|
|
||||||
- share_plus (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- shared_preferences_foundation (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- sqflite_darwin (0.0.4):
|
|
||||||
- Flutter
|
|
||||||
- FlutterMacOS
|
|
||||||
- SwiftyGif (5.4.5)
|
|
||||||
- url_launcher_ios (0.0.1):
|
|
||||||
- Flutter
|
|
||||||
- vibration (3.0.0):
|
|
||||||
- Flutter
|
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/darwin`)
|
|
||||||
- codec2_flutter (from `.symlinks/plugins/codec2_flutter/ios`)
|
- codec2_flutter (from `.symlinks/plugins/codec2_flutter/ios`)
|
||||||
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
|
|
||||||
- file_picker (from `.symlinks/plugins/file_picker/ios`)
|
|
||||||
- Flutter (from `Flutter`)
|
- Flutter (from `Flutter`)
|
||||||
- flutter_avif_ios (from `.symlinks/plugins/flutter_avif_ios/ios`)
|
- flutter_avif_ios (from `.symlinks/plugins/flutter_avif_ios/ios`)
|
||||||
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
- flutter_background_service_ios (from `.symlinks/plugins/flutter_background_service_ios/ios`)
|
||||||
- flutter_blue_plus_darwin (from `.symlinks/plugins/flutter_blue_plus_darwin/darwin`)
|
|
||||||
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
- flutter_compass (from `.symlinks/plugins/flutter_compass/ios`)
|
||||||
- flutter_local_notifications (from `.symlinks/plugins/flutter_local_notifications/ios`)
|
|
||||||
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
|
|
||||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
|
||||||
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
|
- nsd_ios (from `.symlinks/plugins/nsd_ios/ios`)
|
||||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
|
||||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- record_ios (from `.symlinks/plugins/record_ios/ios`)
|
|
||||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
|
||||||
- sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`)
|
|
||||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
|
||||||
- vibration (from `.symlinks/plugins/vibration/ios`)
|
|
||||||
|
|
||||||
SPEC REPOS:
|
|
||||||
trunk:
|
|
||||||
- DKImagePickerController
|
|
||||||
- DKPhotoGallery
|
|
||||||
- SDWebImage
|
|
||||||
- SwiftyGif
|
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
audioplayers_darwin:
|
|
||||||
:path: ".symlinks/plugins/audioplayers_darwin/darwin"
|
|
||||||
codec2_flutter:
|
codec2_flutter:
|
||||||
:path: ".symlinks/plugins/codec2_flutter/ios"
|
:path: ".symlinks/plugins/codec2_flutter/ios"
|
||||||
device_info_plus:
|
|
||||||
:path: ".symlinks/plugins/device_info_plus/ios"
|
|
||||||
file_picker:
|
|
||||||
:path: ".symlinks/plugins/file_picker/ios"
|
|
||||||
Flutter:
|
Flutter:
|
||||||
:path: Flutter
|
:path: Flutter
|
||||||
flutter_avif_ios:
|
flutter_avif_ios:
|
||||||
:path: ".symlinks/plugins/flutter_avif_ios/ios"
|
:path: ".symlinks/plugins/flutter_avif_ios/ios"
|
||||||
flutter_background_service_ios:
|
flutter_background_service_ios:
|
||||||
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
:path: ".symlinks/plugins/flutter_background_service_ios/ios"
|
||||||
flutter_blue_plus_darwin:
|
|
||||||
:path: ".symlinks/plugins/flutter_blue_plus_darwin/darwin"
|
|
||||||
flutter_compass:
|
flutter_compass:
|
||||||
:path: ".symlinks/plugins/flutter_compass/ios"
|
:path: ".symlinks/plugins/flutter_compass/ios"
|
||||||
flutter_local_notifications:
|
|
||||||
:path: ".symlinks/plugins/flutter_local_notifications/ios"
|
|
||||||
geolocator_apple:
|
|
||||||
:path: ".symlinks/plugins/geolocator_apple/darwin"
|
|
||||||
image_picker_ios:
|
|
||||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
|
||||||
nsd_ios:
|
nsd_ios:
|
||||||
:path: ".symlinks/plugins/nsd_ios/ios"
|
:path: ".symlinks/plugins/nsd_ios/ios"
|
||||||
package_info_plus:
|
|
||||||
:path: ".symlinks/plugins/package_info_plus/ios"
|
|
||||||
path_provider_foundation:
|
|
||||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
|
||||||
permission_handler_apple:
|
permission_handler_apple:
|
||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
record_ios:
|
|
||||||
:path: ".symlinks/plugins/record_ios/ios"
|
|
||||||
share_plus:
|
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
|
||||||
shared_preferences_foundation:
|
|
||||||
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
|
|
||||||
sqflite_darwin:
|
|
||||||
:path: ".symlinks/plugins/sqflite_darwin/darwin"
|
|
||||||
url_launcher_ios:
|
|
||||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
|
||||||
vibration:
|
|
||||||
:path: ".symlinks/plugins/vibration/ios"
|
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
audioplayers_darwin: 835ced6edd4c9fc8ebb0a7cc9e294a91d99917d5
|
|
||||||
codec2_flutter: 15e24fa897d9d903a2afb1cc5a17ae3ac88b6d6f
|
codec2_flutter: 15e24fa897d9d903a2afb1cc5a17ae3ac88b6d6f
|
||||||
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
|
|
||||||
DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c
|
|
||||||
DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60
|
|
||||||
file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be
|
|
||||||
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
|
||||||
flutter_avif_ios: 2553d2aafda56339cf924e943da89ad3f40af2dd
|
flutter_avif_ios: 2553d2aafda56339cf924e943da89ad3f40af2dd
|
||||||
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
|
flutter_background_service_ios: 00d31bdff7b4bfe06d32375df358abe0329cf87e
|
||||||
flutter_blue_plus_darwin: 20a08bfeaa0f7804d524858d3d8744bcc1b6dbc3
|
|
||||||
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
flutter_compass: b236ab69b61545cce89fd58527f401a7587d5cc1
|
||||||
flutter_local_notifications: 643a3eda1ce1c0599413ca31672536d423dee214
|
|
||||||
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
|
|
||||||
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
|
|
||||||
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
|
nsd_ios: 596ad79109ddd3e52d665f650f36428049e3653e
|
||||||
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
|
|
||||||
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
|
|
||||||
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d
|
||||||
record_ios: 412daca2350b228e698fffcd08f1f94ceb1e3844
|
|
||||||
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
|
||||||
share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a
|
|
||||||
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
|
|
||||||
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0
|
|
||||||
SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4
|
|
||||||
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
|
|
||||||
vibration: ca8104a8875b9c493e15b21b04e456befd0ff6eb
|
|
||||||
|
|
||||||
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
PODFILE CHECKSUM: 2d56c9747241a29800bf539d978b210bda037878
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
B117D6FC7E214B5BDF078335 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */; };
|
B117D6FC7E214B5BDF078335 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5E125ECD1538D6FD469DAD40 /* Pods_RunnerTests.framework */; };
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXContainerItemProxy section */
|
/* Begin PBXContainerItemProxy section */
|
||||||
@@ -65,6 +66,7 @@
|
|||||||
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@@ -80,6 +82,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
|
||||||
6E07C83B534F125C2CBE6788 /* Pods_Runner.framework in Frameworks */,
|
6E07C83B534F125C2CBE6788 /* Pods_Runner.framework in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
@@ -111,6 +114,7 @@
|
|||||||
9740EEB11CF90186004384FC /* Flutter */ = {
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
@@ -187,6 +191,9 @@
|
|||||||
productType = "com.apple.product-type.bundle.unit-test";
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
};
|
};
|
||||||
97C146ED1CF9000F007C117D /* Runner */ = {
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
packageProductDependencies = (
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
|
||||||
|
);
|
||||||
isa = PBXNativeTarget;
|
isa = PBXNativeTarget;
|
||||||
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
buildPhases = (
|
buildPhases = (
|
||||||
@@ -213,6 +220,9 @@
|
|||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
97C146E61CF9000F007C117D /* Project object */ = {
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
packageReferences = (
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
|
||||||
|
);
|
||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
@@ -489,7 +499,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -511,7 +521,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -530,7 +540,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -547,7 +557,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
BUNDLE_LOADER = "$(TEST_HOST)";
|
BUNDLE_LOADER = "$(TEST_HOST)";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
GENERATE_INFOPLIST_FILE = YES;
|
GENERATE_INFOPLIST_FILE = YES;
|
||||||
MARKETING_VERSION = 1.0;
|
MARKETING_VERSION = 1.0;
|
||||||
@@ -679,7 +689,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -702,7 +712,7 @@
|
|||||||
buildSettings = {
|
buildSettings = {
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CURRENT_PROJECT_VERSION = 116;
|
CURRENT_PROJECT_VERSION = 142;
|
||||||
DEVELOPMENT_TEAM = JND55328G8;
|
DEVELOPMENT_TEAM = JND55328G8;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -752,6 +762,18 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
/* Begin XCLocalSwiftPackageReference section */
|
||||||
|
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
|
||||||
|
isa = XCLocalSwiftPackageReference;
|
||||||
|
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCLocalSwiftPackageReference section */
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = FlutterGeneratedPluginSwiftPackage;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -5,6 +5,24 @@
|
|||||||
<BuildAction
|
<BuildAction
|
||||||
parallelizeBuildables = "YES"
|
parallelizeBuildables = "YES"
|
||||||
buildImplicitDependencies = "YES">
|
buildImplicitDependencies = "YES">
|
||||||
|
<PreActions>
|
||||||
|
<ExecutionAction
|
||||||
|
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
|
||||||
|
<ActionContent
|
||||||
|
title = "Run Prepare Flutter Framework Script"
|
||||||
|
scriptText = "/bin/sh "$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh" prepare ">
|
||||||
|
<EnvironmentBuildable>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</EnvironmentBuildable>
|
||||||
|
</ActionContent>
|
||||||
|
</ExecutionAction>
|
||||||
|
</PreActions>
|
||||||
<BuildActionEntries>
|
<BuildActionEntries>
|
||||||
<BuildActionEntry
|
<BuildActionEntry
|
||||||
buildForTesting = "YES"
|
buildForTesting = "YES"
|
||||||
|
|||||||
59
ios/Runner.xcworkspace/xcshareddata/swiftpm/Package.resolved
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "dkcamera",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKCamera",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "5c691d11014b910aff69f960475d70e65d9dcc96"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkimagepickercontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKImagePickerController",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "4.3.9",
|
||||||
|
"revision" : "0bdfeacefa308545adde07bef86e349186335915"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "dkphotogallery",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/zhangao0086/DKPhotoGallery",
|
||||||
|
"state" : {
|
||||||
|
"branch" : "master",
|
||||||
|
"revision" : "311c1bc7a94f1538f82773a79c84374b12a2ef3d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "sdwebimage",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/SDWebImage/SDWebImage",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "2de3a496eaf6df9a1312862adcfd54acd73c39c0",
|
||||||
|
"version" : "5.21.7"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "swiftygif",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/kirualex/SwiftyGif.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "4430cbc148baa3907651d40562d96325426f409a",
|
||||||
|
"version" : "5.4.5"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"identity" : "tocropviewcontroller",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/TimOliver/TOCropViewController",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d4a6d8100f4b886fdbc8ae399bf144ff3e9afb7e",
|
||||||
|
"version" : "2.8.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>116</string>
|
<string>142</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<true/>
|
<true/>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -16,17 +16,66 @@
|
|||||||
default_platform(:ios)
|
default_platform(:ios)
|
||||||
|
|
||||||
platform :ios do
|
platform :ios do
|
||||||
|
def project_root
|
||||||
|
File.expand_path("../..", __dir__)
|
||||||
|
end
|
||||||
|
|
||||||
|
def beta_app_info
|
||||||
|
{
|
||||||
|
"en-US" => {
|
||||||
|
description: "Field-ready SAR coordination over MeshCore with chat, voice, images, offline maps, and live team context.",
|
||||||
|
feedback_email: "hey@dz0ny.dev",
|
||||||
|
marketing_url: "https://github.com/dz0ny/meshcore-sar",
|
||||||
|
privacy_policy_url: "https://github.com/dz0ny/meshcore-sar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
def beta_build_info
|
||||||
|
{
|
||||||
|
"en-US" => {
|
||||||
|
whats_new: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
desc "Push a new release build to the App Store"
|
desc "Push a new release build to the App Store"
|
||||||
lane :release do
|
lane :release do
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
||||||
upload_to_app_store(skip_metadata: true, skip_screenshots: true)
|
upload_to_testflight(
|
||||||
|
skip_waiting_for_build_processing: true,
|
||||||
|
localized_app_info: beta_app_info,
|
||||||
|
localized_build_info: beta_build_info,
|
||||||
|
changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
desc "Push a new beta build to TestFlight"
|
desc "Push a new beta build to TestFlight"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
increment_build_number(xcodeproj: "Runner.xcodeproj")
|
||||||
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
build_app(workspace: "Runner.xcworkspace", scheme: "Runner")
|
||||||
upload_to_testflight(skip_waiting_for_build_processing: true)
|
upload_to_testflight(
|
||||||
|
skip_waiting_for_build_processing: true,
|
||||||
|
localized_app_info: beta_app_info,
|
||||||
|
localized_build_info: beta_build_info,
|
||||||
|
changelog: "Test MeshCore SAR field coordination flows: messaging, maps, contacts, voice, images, and SAR markers."
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Capture iOS screenshots for App Store Connect"
|
||||||
|
lane :screenshots do
|
||||||
|
sh("cd #{project_root} && scripts/take_screenshots.sh --ios")
|
||||||
|
end
|
||||||
|
|
||||||
|
desc "Upload App Store screenshots"
|
||||||
|
lane :store_assets do
|
||||||
|
upload_to_app_store(
|
||||||
|
skip_binary_upload: true,
|
||||||
|
skip_metadata: true,
|
||||||
|
skip_screenshots: false,
|
||||||
|
submit_for_review: false,
|
||||||
|
force: true
|
||||||
|
)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -31,6 +31,22 @@ Push a new release build to the App Store
|
|||||||
|
|
||||||
Push a new beta build to TestFlight
|
Push a new beta build to TestFlight
|
||||||
|
|
||||||
|
### ios screenshots
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios screenshots
|
||||||
|
```
|
||||||
|
|
||||||
|
Capture iOS screenshots for App Store Connect
|
||||||
|
|
||||||
|
### ios store_assets
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[bundle exec] fastlane ios store_assets
|
||||||
|
```
|
||||||
|
|
||||||
|
Upload App Store screenshots
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.
|
||||||
|
|||||||
1
ios/fastlane/metadata/copyright.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Copyright 2026 Janez Troha
|
||||||
14
ios/fastlane/metadata/en-US/description.txt
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
MeshCore SAR gives search and rescue teams a field-ready coordination workspace for low-connectivity operations.
|
||||||
|
|
||||||
|
Use mesh chat, live location context, SAR markers, offline-first maps, tactical drawings, voice clips, and compressed image transfer in one app. The app is built around MeshCore and BLE so teams can coordinate over LoRa links when normal infrastructure is weak or unavailable.
|
||||||
|
|
||||||
|
Key capabilities:
|
||||||
|
- Direct and group mesh messaging
|
||||||
|
- Live team contacts with signal, battery, and location context
|
||||||
|
- Offline maps with SAR markers and tactical overlays
|
||||||
|
- Voice clips optimized for low-bandwidth links
|
||||||
|
- Image transfer with compact encoding
|
||||||
|
- GPX trail import/export
|
||||||
|
- Incident, staging, and discovery workflows for field teams
|
||||||
|
|
||||||
|
MeshCore SAR is intended for search and rescue field teams, incident command, and operators who need practical coordination tools outside normal coverage.
|
||||||
1
ios/fastlane/metadata/en-US/keywords.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
meshcore,sar,search,rescue,mesh,lora,offline,maps,radio,gps
|
||||||
1
ios/fastlane/metadata/en-US/marketing_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar
|
||||||
1
ios/fastlane/metadata/en-US/name.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MeshCore SAR
|
||||||
1
ios/fastlane/metadata/en-US/privacy_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar
|
||||||
1
ios/fastlane/metadata/en-US/promotional_text.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Field coordination for search and rescue teams using MeshCore, BLE, offline maps, messaging, voice, and SAR markers.
|
||||||
1
ios/fastlane/metadata/en-US/release_notes.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Adds App Store screenshots and TestFlight metadata for MeshCore SAR publishing.
|
||||||
1
ios/fastlane/metadata/en-US/subtitle.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
SAR mesh field coordination
|
||||||
1
ios/fastlane/metadata/en-US/support_url.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
https://github.com/dz0ny/meshcore-sar/issues
|
||||||
@@ -5,22 +5,22 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000235">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000247">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.385323">
|
<testcase classname="fastlane.lanes" name="1: increment_build_number" time="0.741448">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: build_app" time="93.530304">
|
<testcase classname="fastlane.lanes" name="2: build_app" time="110.585223">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: upload_to_app_store" time="210.562484">
|
<testcase classname="fastlane.lanes" name="3: upload_to_testflight" time="353.311697">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
BIN
ios/fastlane/screenshots/en-US/01-messages.png
Normal file
|
After Width: | Height: | Size: 584 KiB |
BIN
ios/fastlane/screenshots/en-US/02-contacts.png
Normal file
|
After Width: | Height: | Size: 595 KiB |
BIN
ios/fastlane/screenshots/en-US/03-map.png
Normal file
|
After Width: | Height: | Size: 3.9 MiB |
BIN
ios/fastlane/screenshots/en-US/04-settings.png
Normal file
|
After Width: | Height: | Size: 538 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-01-messages.png
Normal file
|
After Width: | Height: | Size: 500 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-02-contacts.png
Normal file
|
After Width: | Height: | Size: 526 KiB |
BIN
ios/fastlane/screenshots/en-US/ipad-03-map.png
Normal file
|
After Width: | Height: | Size: 7.0 MiB |
BIN
ios/fastlane/screenshots/en-US/ipad-04-settings.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Schaltfläche zum Neustart des BLE-Scans"
|
"description": "Schaltfläche zum Neustart des BLE-Scans"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Zum Verbinden tippen",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Untertiteltext für Gerät in der Scanliste"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Gerät nicht verbunden",
|
"deviceNotConnected": "Gerät nicht verbunden",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Fehlermeldung, wenn das Gerät nicht verbunden ist"
|
"description": "Fehlermeldung, wenn das Gerät nicht verbunden ist"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Einstellung zum Anzeigen von RX/TX-Indikatoren"
|
"description": "Einstellung zum Anzeigen von RX/TX-Indikatoren"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Paketaktivitätsindikatoren in der oberen Leiste anzeigen",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Beschreibung für die Einstellung der RX/TX-Indikatoren"
|
|
||||||
},
|
|
||||||
"disableMap": "Karte deaktivieren",
|
"disableMap": "Karte deaktivieren",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Einstellung zum Deaktivieren des Karten-Tabs"
|
"description": "Einstellung zum Deaktivieren des Karten-Tabs"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Karten-Tab ausblenden, um Akku zu sparen",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Beschreibung für die Einstellung zum Deaktivieren der Karte"
|
|
||||||
},
|
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Beschriftung der Spracheinstellung"
|
"description": "Beschriftung der Spracheinstellung"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "Beschriftung der App-Version"
|
"description": "Beschriftung der App-Version"
|
||||||
},
|
},
|
||||||
"appName": "App-Name",
|
|
||||||
"@appName": {
|
|
||||||
"description": "Beschriftung des App-Namens"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Über MeshCore SAR",
|
"aboutMeshCoreSar": "Über MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "Titel des Über-Dialogs"
|
"description": "Titel des Über-Dialogs"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Aktion zum Zurücksetzen des Kontaktpfads für Umleitung"
|
"description": "Aktion zum Zurücksetzen des Kontaktpfads für Umleitung"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Öffentlicher Schlüssel in die Zwischenablage kopiert",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Erfolgsmeldung, wenn der öffentliche Schlüssel kopiert wurde"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} in die Zwischenablage kopiert",
|
"copiedToClipboard": "{label} in die Zwischenablage kopiert",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Erfolgsmeldung, wenn ein Wert in die Zwischenablage kopiert wurde",
|
"description": "Erfolgsmeldung, wenn ein Wert in die Zwischenablage kopiert wurde",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Beschriftung Zuletzt gesehen"
|
"description": "Beschriftung Zuletzt gesehen"
|
||||||
},
|
},
|
||||||
"roomStatus": "Raumstatus",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Überschrift des Raumstatus-Bereichs"
|
|
||||||
},
|
|
||||||
"loginStatus": "Anmeldestatus",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Beschriftung des Anmeldestatus"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Nicht angemeldet",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Nicht angemeldet-Status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Admin-Zugriff",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Beschriftung des Admin-Zugriffs"
|
|
||||||
},
|
|
||||||
"yes": "Ja",
|
"yes": "Ja",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Ja-Antwort"
|
"description": "Ja-Antwort"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Beschriftung der Berechtigungen"
|
"description": "Beschriftung der Berechtigungen"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Passwort gespeichert",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Beschriftung Passwort gespeichert"
|
|
||||||
},
|
|
||||||
"locationColon": "Standort:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Überschrift des Standortbereichs"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetrie",
|
"telemetry": "Telemetrie",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Überschrift des Telemetrie-Bereichs"
|
"description": "Überschrift des Telemetrie-Bereichs"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS aus Telemetrie-Beschriftung"
|
"description": "GPS aus Telemetrie-Beschriftung"
|
||||||
},
|
},
|
||||||
"updated": "Aktualisiert",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Beschriftung des Aktualisierungszeitstempels"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Pfad zurückgesetzt für {name}. Nächste Nachricht findet eine neue Route.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Infonachricht nach Pfad-Reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Erneut bei Raum anmelden",
|
"reLoginToRoom": "Erneut bei Raum anmelden",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Schaltfläche zur erneuten Anmeldung beim Raum"
|
"description": "Schaltfläche zur erneuten Anmeldung beim Raum"
|
||||||
@@ -1114,10 +1057,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Beschriftung für Funkfrequenzfeld"
|
"description": "Beschriftung für Funkfrequenzfeld"
|
||||||
},
|
},
|
||||||
"frequencyExample": "z.B. 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Hilfetext-Beispiel für Frequenz"
|
|
||||||
},
|
|
||||||
"bandwidth": "Bandbreite",
|
"bandwidth": "Bandbreite",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Beschriftung für Bandbreiten-Dropdown"
|
"description": "Beschriftung für Bandbreiten-Dropdown"
|
||||||
@@ -1221,47 +1160,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Tooltip für Verlaufssteuerung"
|
"description": "Tooltip für Verlaufssteuerung"
|
||||||
},
|
},
|
||||||
"contactTrails": "Kontaktverläufe",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Überschrift des Bereichs für Kontaktverläufe"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Alle Kontaktverläufe anzeigen",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Beschriftung zum Umschalten der Anzeige aller Kontaktverläufe"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Keine Kontakte mit Standortverlauf",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Untertitel, wenn keine Kontakte mit Verläufen vorhanden sind"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Verläufe für {count} Kontakte anzeigen",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Untertitel mit Anzahl der Kontakte mit Verläufen",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Einzelne Kontaktverläufe",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Titel des Erweiterungselements für einzelne Kontaktverläufe"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Geräteinformationen",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Überschrift des Geräteinformationen-Bereichs"
|
|
||||||
},
|
|
||||||
"bleName": "BLE-Name",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Beschriftung des Bluetooth-Low-Energy-Gerätenamens"
|
|
||||||
},
|
|
||||||
"meshName": "Mesh-Name",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Beschriftung des Mesh-Netzwerknamens"
|
|
||||||
},
|
|
||||||
"notSet": "Nicht festgelegt",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Beschriftung, wenn ein Wert nicht festgelegt ist"
|
|
||||||
},
|
|
||||||
"model": "Modell",
|
"model": "Modell",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Beschriftung des Gerätemodells"
|
"description": "Beschriftung des Gerätemodells"
|
||||||
@@ -1270,14 +1168,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Beschriftung der Version"
|
"description": "Beschriftung der Version"
|
||||||
},
|
},
|
||||||
"buildDate": "Build-Datum",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Beschriftung des Firmware-Build-Datums"
|
|
||||||
},
|
|
||||||
"firmware": "Firmware",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Beschriftung der Firmware"
|
|
||||||
},
|
|
||||||
"maxContacts": "Max. Kontakte",
|
"maxContacts": "Max. Kontakte",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Beschriftung der maximalen Kontaktkapazität"
|
"description": "Beschriftung der maximalen Kontaktkapazität"
|
||||||
@@ -1290,26 +1180,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Überschrift des öffentlichen Informationen-Bereichs"
|
"description": "Überschrift des öffentlichen Informationen-Bereichs"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Mesh-Netzwerkname",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Beschriftung des Mesh-Netzwerknamens-Felds"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Name, der in Mesh-Sendungen übertragen wird",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Hilfetext für Mesh-Netzwerknamens-Feld"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetrie & Standortfreigabe",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Beschriftung des Telemetrie- und Standortfreigabe-Umschalters"
|
|
||||||
},
|
|
||||||
"lat": "Breite",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Beschriftung des Breitengradfelds (Kurzform)"
|
|
||||||
},
|
|
||||||
"lon": "Lange",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Beschriftung des Längengradfelds (Kurzform)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Aktuellen Standort verwenden",
|
"useCurrentLocation": "Aktuellen Standort verwenden",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip für Schaltfläche zum Verwenden des aktuellen Standorts"
|
"description": "Tooltip für Schaltfläche zum Verwenden des aktuellen Standorts"
|
||||||
@@ -1339,15 +1209,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} in Zwischenablage kopiert",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Kurze Erfolgsmeldung beim Kopieren in die Zwischenablage",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Fehler beim Speichern: {error}",
|
"failedToSave": "Fehler beim Speichern: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Allgemeine Fehlermeldung für Speicherfehler",
|
"description": "Allgemeine Fehlermeldung für Speicherfehler",
|
||||||
@@ -1367,7 +1228,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sarTemplates": "SAR-Vorlagen",
|
"sarTemplates": "SAR-Vorlagen",
|
||||||
"manageSarTemplates": "SAR-Vorlagen verwalten",
|
|
||||||
"addTemplate": "Vorlage hinzufügen",
|
"addTemplate": "Vorlage hinzufügen",
|
||||||
"editTemplate": "Vorlage bearbeiten",
|
"editTemplate": "Vorlage bearbeiten",
|
||||||
"deleteTemplate": "Vorlage löschen",
|
"deleteTemplate": "Vorlage löschen",
|
||||||
@@ -1395,7 +1255,6 @@
|
|||||||
"noTemplates": "Keine Vorlagen verfugbar",
|
"noTemplates": "Keine Vorlagen verfugbar",
|
||||||
"tapAddToCreate": "Tippen Sie auf +, um Ihre erste Vorlage zu erstellen",
|
"tapAddToCreate": "Tippen Sie auf +, um Ihre erste Vorlage zu erstellen",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permissionsSection": "Berechtigungen",
|
|
||||||
"locationPermission": "Standortberechtigung",
|
"locationPermission": "Standortberechtigung",
|
||||||
"checking": "Überprüfen...",
|
"checking": "Überprüfen...",
|
||||||
"locationPermissionGrantedAlways": "Erteilt (Immer)",
|
"locationPermissionGrantedAlways": "Erteilt (Immer)",
|
||||||
@@ -1535,28 +1394,6 @@
|
|||||||
"wizardNext": "Weiter",
|
"wizardNext": "Weiter",
|
||||||
"wizardGetStarted": "Loslegen",
|
"wizardGetStarted": "Loslegen",
|
||||||
"wizardWelcomeTitle": "Willkommen bei MeshCore SAR",
|
"wizardWelcomeTitle": "Willkommen bei MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Ein leistungsstarkes Offline-Kommunikationstool für Such- und Rettungseinsätze. Verbinden Sie sich mit Ihrem Team über Mesh-Funktechnologie, wenn herkömmliche Netzwerke nicht verfügbar sind.",
|
|
||||||
"wizardConnectingTitle": "Verbindung zum Radio",
|
|
||||||
"wizardConnectingDescription": "Verbinden Sie Ihr Smartphone über Bluetooth mit einem MeshCore-Funkgerät, um offline zu kommunizieren.",
|
|
||||||
"wizardConnectingFeature1": "Nach MeshCore-Geräten in der Nähe suchen",
|
|
||||||
"wizardConnectingFeature2": "Mit Ihrem Funkgerät über Bluetooth koppeln",
|
|
||||||
"wizardConnectingFeature3": "Funktioniert vollständig offline - kein Internet erforderlich",
|
|
||||||
"wizardChannelTitle": "Kanäle",
|
|
||||||
"wizardChannelDescription": "Senden Sie Nachrichten an alle auf einem Kanal, perfekt für teamweite Ankündigungen und Koordination.",
|
|
||||||
"wizardChannelFeature1": "Öffentlicher Kanal für allgemeine Teamkommunikation",
|
|
||||||
"wizardChannelFeature2": "Erstellen Sie benutzerdefinierte Kanäle für bestimmte Gruppen",
|
|
||||||
"wizardChannelFeature3": "Nachrichten werden automatisch über das Mesh weitergeleitet",
|
|
||||||
"wizardContactsTitle": "Kontakte",
|
|
||||||
"wizardContactsDescription": "Ihre Teammitglieder erscheinen automatisch, wenn sie dem Mesh-Netzwerk beitreten. Senden Sie ihnen direkte Nachrichten oder sehen Sie ihren Standort.",
|
|
||||||
"wizardContactsFeature1": "Kontakte werden automatisch erkannt",
|
|
||||||
"wizardContactsFeature2": "Private Direktnachrichten senden",
|
|
||||||
"wizardContactsFeature3": "Batteriestand und letzte Aktivität anzeigen",
|
|
||||||
"wizardMapTitle": "Karte & Standort",
|
|
||||||
"wizardMapDescription": "Verfolgen Sie Ihr Team in Echtzeit und markieren Sie wichtige Standorte für Such- und Rettungseinsätze.",
|
|
||||||
"wizardMapFeature1": "SAR-Markierungen für gefundene Personen, Feuer und Sammelstellen",
|
|
||||||
"wizardMapFeature2": "GPS-Verfolgung von Teammitgliedern in Echtzeit",
|
|
||||||
"wizardMapFeature3": "Offline-Karten für entlegene Gebiete herunterladen",
|
|
||||||
"wizardMapFeature4": "Formen zeichnen und taktische Informationen teilen",
|
|
||||||
"viewWelcomeTutorial": "Willkommens-Tutorial ansehen",
|
"viewWelcomeTutorial": "Willkommens-Tutorial ansehen",
|
||||||
"allTeamContacts": "Alle Team-Kontakte",
|
"allTeamContacts": "Alle Team-Kontakte",
|
||||||
"directMessagesInfo": "Direktnachrichten mit Bestätigungen. An {count} Teammitglieder gesendet.",
|
"directMessagesInfo": "Direktnachrichten mit Bestätigungen. An {count} Teammitglieder gesendet.",
|
||||||
@@ -1638,5 +1475,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Aktiv",
|
||||||
|
"addContact": "Kontakt hinzufügen",
|
||||||
|
"all": "Alle",
|
||||||
|
"clearAllLabel": "Alle 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",
|
||||||
|
"autoSend": "Automatisch senden",
|
||||||
|
"autoSendDescription": "Über aktuellen Pfad senden.",
|
||||||
|
"sendDirect": "Direkt senden",
|
||||||
|
"sendDirectDescription": "Direkt an diesen Kontakt senden.",
|
||||||
|
"sendFlood": "Per Flutung senden",
|
||||||
|
"sendFloodDescription": "Über alle Repeater senden.",
|
||||||
|
"share": "Teilen",
|
||||||
|
"trace": "Trace",
|
||||||
|
"discovery": "Erkennung",
|
||||||
|
"discoverRepeaters": "Repeater entdecken",
|
||||||
|
"discoverSensors": "Sensoren entdecken",
|
||||||
|
"repeaterDiscoverySent": "Repeater-Erkennung gesendet",
|
||||||
|
"sensorDiscoverySent": "Sensor-Erkennung gesendet",
|
||||||
|
"clearedPendingDiscoveries": "Ausstehende Entdeckungen gelöscht.",
|
||||||
|
"postConnectDiscoveryTitle": "Repeater jetzt entdecken?",
|
||||||
|
"postConnectDiscoveryDescription": "Repeater-Erkennung direkt nach dem Verbinden ausführen, damit Sie nahegelegene MeshCore-Knoten sehen und schneller zu Ihrem Netzwerk hinzufügen können.",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"hideRepeatersOnMap": "Repeater auf Karte ausblenden",
|
||||||
|
"setMapScale": "Kartenmaßstab einstellen",
|
||||||
|
"customMapScaleSaved": "Benutzerdefinierter Kartenmaßstab gespeichert",
|
||||||
|
"voiceBitrate": "Sprach-Bitrate",
|
||||||
|
"voiceCompressor": "Sprachkompressor",
|
||||||
|
"voiceLimiter": "Sprach-Limiter",
|
||||||
|
"micAutoGain": "Mikrofon-Automatik",
|
||||||
|
"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",
|
||||||
|
"busy": "Beschäftigt",
|
||||||
|
"searchMessages": "Nachrichten suchen",
|
||||||
|
"sendImageFromGallery": "Bild aus Galerie senden",
|
||||||
|
"takePhoto": "Foto aufnehmen",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Hinzufügen",
|
||||||
|
"addAdditionalDetails": "Zusätzliche Details hinzufügen",
|
||||||
|
"addPeer": "Peer hinzufügen",
|
||||||
|
"addPeerManually": "Peer manuell hinzufügen",
|
||||||
|
"addedToContacts": "{name} zu Kontakten hinzugefügt",
|
||||||
|
"addedToSensors": "{name} zu Sensoren hinzugefügt",
|
||||||
|
"allowAll": "Alle zulassen",
|
||||||
|
"anonymousRxStats": "Anonyme RX-Statistiken",
|
||||||
|
"appearance": "Erscheinungsbild",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Basistelemetrie",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Summer-Alarme",
|
||||||
|
"cancelImageReceive": "Bildempfang abbrechen",
|
||||||
|
"choose": "Auswählen",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Wähle, welche Tabs und Kontaktbereiche angezeigt werden",
|
||||||
|
"clearCache": "Cache leeren",
|
||||||
|
"clearHistory": "Verlauf löschen",
|
||||||
|
"clearLiveView": "Live-Ansicht leeren",
|
||||||
|
"clearLogs": "Protokolle löschen",
|
||||||
|
"clearOfflineCache": "Offline-Cache leeren?",
|
||||||
|
"clearOverlay": "Overlay löschen",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Route erst nach allen fehlgeschlagenen Versuchen löschen",
|
||||||
|
"codecBitrateAndAudioProcessing": "Codec, Bitrate und Audioverarbeitung",
|
||||||
|
"connectByIpAddress": "Über IP-Adresse verbinden",
|
||||||
|
"controlWhichAlertsYouReceive": "Steuere, welche Benachrichtigungen du erhältst",
|
||||||
|
"copyFullHex": "Vollständigen Hex kopieren",
|
||||||
|
"copyRawResponse": "Rohantwort kopieren",
|
||||||
|
"couldNotLoadMetHistory": "MET-Verlauf konnte nicht geladen werden",
|
||||||
|
"customizeSensor": "{name} anpassen",
|
||||||
|
"data": "Daten",
|
||||||
|
"deleteGroup": "Gruppe löschen",
|
||||||
|
"deleteStyleConfirm": "{name} löschen?",
|
||||||
|
"deleteThisDrawing": "Diese(s) {type} löschen?",
|
||||||
|
"deny": "Ablehnen",
|
||||||
|
"deviceInfo": "Geräteinformationen",
|
||||||
|
"deviceSettings": "Geräteeinstellungen",
|
||||||
|
"deviceTimeSynced": "Gerätezeit mit diesem Telefon synchronisiert.",
|
||||||
|
"disableFastGpsPublishing": "Schnelle GPS-Veröffentlichung deaktivieren",
|
||||||
|
"displayMarkersAndTraceDatabase": "Anzeige, Markierungen und Trace-Datenbank",
|
||||||
|
"download": "Herunterladen",
|
||||||
|
"environmentalTelemetry": "Umwelttelemetrie",
|
||||||
|
"errorCheckingForUpdates": "Fehler bei der Suche nach Updates: {error}",
|
||||||
|
"errorGeneric": "Fehler: {error}",
|
||||||
|
"errorOpeningDownload": "Fehler beim Öffnen des Downloads: {error}",
|
||||||
|
"exportLogs": "Protokolle exportieren",
|
||||||
|
"extraAggressiveAvifCompression": "Extra-aggressive AVIF-Komprimierung",
|
||||||
|
"failedToAddContact": "Kontakt konnte nicht hinzugefügt werden: {error}",
|
||||||
|
"failedToAddName": "{name} konnte nicht hinzugefügt werden: {error}",
|
||||||
|
"failedToClearChannels": "Kanäle konnten nicht gelöscht werden: {error}",
|
||||||
|
"failedToClearContacts": "Kontakte konnten nicht gelöscht werden: {error}",
|
||||||
|
"failedToConnectViaSerial": "Verbindung über seriell fehlgeschlagen",
|
||||||
|
"failedToLoadPreviewImage": "Vorschaubild konnte nicht geladen werden: {error}",
|
||||||
|
"failedToLoadTrace": "Trace konnte nicht geladen werden: {error}",
|
||||||
|
"failedToSendSarMarker": "SAR-Markierung konnte nicht gesendet werden: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Summermodus konnte nicht gesetzt werden: {error}",
|
||||||
|
"failedToSetGpsMode": "GPS-Modus konnte nicht gesetzt werden: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Gerätezeit konnte nicht synchronisiert werden: {error}",
|
||||||
|
"failedToWipeDeviceData": "Gerätedaten konnten nicht gelöscht werden: {error}",
|
||||||
|
"fastGpsTargetChannel": "Schneller GPS-Zielkanal",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Schnelle Updates, Schwellenwerte und Berechtigungen",
|
||||||
|
"fetchingCatalog": "Katalog wird geladen...",
|
||||||
|
"fifteenMinutes": "15 Minuten",
|
||||||
|
"fifteenSeconds": "15 Sekunden",
|
||||||
|
"filterByDirection": "Nach Richtung filtern",
|
||||||
|
"finish": "Fertig",
|
||||||
|
"fiveMinutes": "5 Minuten",
|
||||||
|
"fiveSeconds": "5 Sekunden",
|
||||||
|
"fix": "Fix",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "GPS-Anzeigenrichtlinie",
|
||||||
|
"gpsAndLocation": "GPS & Standort",
|
||||||
|
"gpsDiagnostics": "GPS-Diagnose",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Hilfe",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Ausgeblendet",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Wie das Funkgerät entdeckte Knoten automatisch hinzufügt.",
|
||||||
|
"images": "Bilder",
|
||||||
|
"importProfile": "Profil importieren",
|
||||||
|
"lastFix": "Letzter Fix",
|
||||||
|
"latest": "Neueste",
|
||||||
|
"latitude": "Breitengrad",
|
||||||
|
"loadASavedRegion": "Gespeicherte Region laden",
|
||||||
|
"loading": "Laden",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Standortfreigabe, Hardware und Aktualisierungsintervall.",
|
||||||
|
"locationTelemetry": "Standorttelemetrie",
|
||||||
|
"lockDestination": "Ziel sperren",
|
||||||
|
"longitude": "Längengrad",
|
||||||
|
"mapStyle": "Kartenstil",
|
||||||
|
"max": "Max",
|
||||||
|
"maxZoom": "Max. Zoom",
|
||||||
|
"messaging": "Nachrichten",
|
||||||
|
"metHistory": "MET-Verlauf",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Min. Zoom",
|
||||||
|
"moveDown": "Nach unten",
|
||||||
|
"moveUp": "Nach oben",
|
||||||
|
"multiAckMode": "Multi-ACK-Modus",
|
||||||
|
"multiDeviceWorkspaceManagement": "Mehrgeräte-Arbeitsbereichsverwaltung",
|
||||||
|
"myLocation": "Mein Standort",
|
||||||
|
"nameAndTelemetryShared": "Name und Telemetrie werden mit anderen Geräten geteilt.",
|
||||||
|
"newVersionAvailable": "Neue Version: {version}",
|
||||||
|
"newer": "Neuer",
|
||||||
|
"noNeighboursFound": "Keine Nachbarn gefunden",
|
||||||
|
"noServersFound": "Keine Server gefunden",
|
||||||
|
"notSet": "Nicht gesetzt",
|
||||||
|
"notifications": "Benachrichtigungen",
|
||||||
|
"offZeroSeconds": "Aus (0 s)",
|
||||||
|
"offlineMaps": "Offlinekarten",
|
||||||
|
"older": "Älter",
|
||||||
|
"oneByteMode0": "1 Byte (Modus 0)",
|
||||||
|
"oneHour": "1 Stunde",
|
||||||
|
"oneMinute": "1 Minute",
|
||||||
|
"openPacketLogs": "Paketprotokolle öffnen",
|
||||||
|
"packetTypeHelp": "Pakettyp-Hilfe",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Teilen-Link oder Hex-Anzeige einfügen",
|
||||||
|
"pathHash": "Pfad-Hash",
|
||||||
|
"pathSize": "Pfadgröße",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Polygon",
|
||||||
|
"rawResponseCopied": "Rohantwort kopiert",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Bei Fehler über nächsten Repeater erneut senden",
|
||||||
|
"resolveContact": "Kontakt auflösen",
|
||||||
|
"routingRetriesAndDestinationLock": "Routing, Wiederholungsversuche und Zielsperre",
|
||||||
|
"samples": "Proben",
|
||||||
|
"satellites": "Satelliten",
|
||||||
|
"saving": "Speichern...",
|
||||||
|
"search": "Suchen",
|
||||||
|
"searchByNameOrToken": "Nach Name oder Token suchen",
|
||||||
|
"searchDiscoveredNodes": "Entdeckte Knoten suchen",
|
||||||
|
"searchInCurrentFilter": "Im aktuellen Filter suchen",
|
||||||
|
"searchLogs": "Protokolle durchsuchen...",
|
||||||
|
"searchSensors": "Sensoren suchen",
|
||||||
|
"searchThisSection": "Diesen Bereich durchsuchen",
|
||||||
|
"selectedRelays": "Ausgewählte Relais",
|
||||||
|
"sensor": "Sensor",
|
||||||
|
"serialError": "Serieller Fehler: {error}",
|
||||||
|
"setPathForContact": "Pfad für {name} festlegen",
|
||||||
|
"shareLiveGps": "Live-GPS teilen",
|
||||||
|
"shareMyTiles": "Meine Kacheln teilen",
|
||||||
|
"sharingLocationFromPhone": "Standort auf diesem Kanal vom Telefon wird geteilt.",
|
||||||
|
"sharingLocationFromRadio": "Standort auf diesem Kanal vom Funkgerät wird geteilt.",
|
||||||
|
"signalDbm": "Signal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Größe, Komprimierung und Vorschau",
|
||||||
|
"skip": "Überspringen",
|
||||||
|
"smallerFileSize": "Kleinere Dateigröße",
|
||||||
|
"sort": "Sortieren",
|
||||||
|
"stoppedSharingLocation": "Standortfreigabe auf diesem Kanal beendet.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Benachrichtigungen im Vordergrund unterdrücken",
|
||||||
|
"tabsAndNavigation": "Tabs & Navigation",
|
||||||
|
"templatesAndTutorials": "Vorlagen und Anleitungen",
|
||||||
|
"tenMinutes": "10 Minuten",
|
||||||
|
"tenSeconds": "10 Sekunden",
|
||||||
|
"testSendUpdate": "Testupdate senden",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Design, Sprache und Anzeigeeinstellungen",
|
||||||
|
"thirtyMinutes": "30 Minuten",
|
||||||
|
"thirtySeconds": "30 Sekunden",
|
||||||
|
"threeBytesMode2": "3 Bytes (Modus 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Verkehrsstatistiken, Nachrichtenverlauf und Entwicklertools",
|
||||||
|
"twoBytesMode1": "2 Bytes (Modus 1)",
|
||||||
|
"twoMinutes": "2 Minuten",
|
||||||
|
"undo": "Rückgängig",
|
||||||
|
"updated": "Aktualisiert",
|
||||||
|
"useContactFlags": "Kontakt-Flags verwenden",
|
||||||
|
"useSavedCoordinates": "Gespeicherte Koordinaten verwenden",
|
||||||
|
"view": "Ansicht",
|
||||||
|
"viewNeighbours": "Nachbarn anzeigen",
|
||||||
|
"viewPublicStats": "Öffentliche Statistiken anzeigen",
|
||||||
|
"setRegionScope": "Regionsbereich festlegen",
|
||||||
|
"regionScope": "Regionsbereich",
|
||||||
|
"regionScopeNone": "Keiner (global)",
|
||||||
|
"clearRegionScope": "Bereich löschen",
|
||||||
|
"regionScopeWarning": "Nur Repeater, die diese Region zulassen, werden weiterleiten.",
|
||||||
|
"discoverRegions": "Von Repeatern entdecken",
|
||||||
|
"discoveringRegions": "Regionen werden entdeckt...",
|
||||||
|
"enterRegionName": "Regionsname (z.B. auckland)",
|
||||||
|
"noRegionsFound": "Keine Regionen auf diesem Repeater gefunden.",
|
||||||
|
"regionScopeSet": "Regionsbereich auf {name} gesetzt",
|
||||||
|
"regionScopeCleared": "Regionsbereich gelöscht",
|
||||||
|
"simpleMode": "Einfacher Modus",
|
||||||
|
"simpleModeDescription": "Nur Chat und Karte mit favorisierten Kontakten",
|
||||||
|
"simpleModeRequiresLockedDestination": "Aktivieren Sie zuerst „Ziel sperren“ mit einem Kanal, um den einfachen Modus zu nutzen",
|
||||||
|
"noFavouriteContacts": "Keine favorisierten Kontakte",
|
||||||
|
"selectDestinationFirst": "Wählen Sie zuerst einen Kanal, Kontakt oder Raum",
|
||||||
|
"recipientNoLongerAvailable": "Empfänger ist nicht mehr verfügbar",
|
||||||
|
"failedToSendMessage": "Senden fehlgeschlagen: {error}",
|
||||||
|
"tictactoeDirectOnly": "Tic-Tac-Toe funktioniert nur in Direktnachrichten. Wählen Sie zuerst einen Kontakt.",
|
||||||
|
"deviceKeyUnavailable": "Geräteschlüssel ist nicht verfügbar",
|
||||||
|
"imageCompressionFailed": "Bildkomprimierung fehlgeschlagen",
|
||||||
|
"imageFragmentationFailed": "Bildfragmentierung fehlgeschlagen",
|
||||||
|
"failedToAnnounceImage": "Bild konnte nicht angekündigt werden",
|
||||||
|
"noRecipientSelected": "Kein Empfänger ausgewählt",
|
||||||
|
"imageSendFailed": "Bildversand fehlgeschlagen",
|
||||||
|
"microphonePermissionRequiredForVoice": "Für Sprachnachrichten ist die Mikrofonberechtigung erforderlich",
|
||||||
|
"recordingDiscarded": "Aufnahme verworfen",
|
||||||
|
"publicChannelMediaWarning": "Sie sind dabei, {mediaType} an den öffentlichen Kanal zu senden. Das ist nicht ratsam, da alle im Mesh-Netz es empfangen können. Wählen Sie einen privaten oder getaggten Kanal, sofern dies nicht beabsichtigt ist.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Wählen Sie einen privaten Kanal, um Ihren Standort zu teilen.",
|
||||||
|
"stopSharingMyLocation": "Meinen Standort nicht mehr teilen",
|
||||||
|
"shareMyLocation": "Meinen Standort teilen",
|
||||||
|
"stopRecording": "Aufnahme stoppen",
|
||||||
|
"recordVoice": "Sprachnachricht aufnehmen",
|
||||||
|
"moreActions": "Weitere Aktionen",
|
||||||
|
"noMessagesInFilter": "Keine Nachrichten in diesem Filter",
|
||||||
|
"noMatchesInFilter": "Keine Treffer in diesem Filter",
|
||||||
|
"syncFailed": "Synchronisierung fehlgeschlagen: {error}",
|
||||||
|
"invalidLink": "Ungültiger Link",
|
||||||
|
"cannotOpenLink": "Link kann nicht geöffnet werden",
|
||||||
|
"failedToOpenLink": "Link konnte nicht geöffnet werden",
|
||||||
|
"failedToResendMessage": "Nachricht konnte nicht erneut gesendet werden",
|
||||||
|
"retryFailed": "Wiederholung fehlgeschlagen: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Zeichnung & Nachricht löschen",
|
||||||
|
"lastEchoRelay": "Letztes Echo – Relais",
|
||||||
|
"lastEchoPath": "Letztes Echo – Pfad",
|
||||||
|
"lastEchoBytesReport": "Letztes Echo – Byte-Bericht",
|
||||||
|
"noGpsCoordinatesAvailable": "Keine GPS-Koordinaten verfügbar",
|
||||||
|
"notASarMarker": "Keine SAR-Markierung",
|
||||||
|
"failedToSaveTemplate": "Vorlage konnte nicht gespeichert werden: {error}",
|
||||||
|
"drawingNotFound": "Zeichnung nicht gefunden",
|
||||||
|
"unknownDrawingType": "Unbekannter Zeichnungstyp",
|
||||||
|
"customMapMarker": "Eigene Kartenmarkierung",
|
||||||
|
"customMapPointLabel": "Punkt: {point}",
|
||||||
|
"mapIdLabel": "Karten-ID: {id}",
|
||||||
|
"cannotFetchVoice": "Sprachnachricht kann nicht abgerufen werden",
|
||||||
|
"senderContactUnknown": "Absenderkontakt ist unbekannt. Synchronisieren Sie zuerst die Kontakte.",
|
||||||
|
"senderRouteUnknown": "Absenderroute ist unbekannt. Synchronisieren Sie zuerst Kontakte/Pfad.",
|
||||||
|
"messageTooFar": "Nachricht ist zu weit entfernt ({hops} Hops, max. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Absenderroute hat auf die Pfadprüfung nicht reagiert. Synchronisieren Sie Kontakte/Pfad und versuchen Sie es erneut.",
|
||||||
|
"senderRouteNoRawResponse": "Absenderroute hat auf dem Raw-Transportpfad nicht reagiert.",
|
||||||
|
"voiceFetchOverHops": "Sprachabruf über {hops} Hops kann eine Weile dauern.",
|
||||||
|
"voiceReceiveCanceled": "Sprachempfang abgebrochen",
|
||||||
|
"fetchingMissingVoiceFragments": "Fehlende Sprachfragmente werden abgerufen",
|
||||||
|
"receivingVoice": "Sprachnachricht wird empfangen",
|
||||||
|
"cannotFetchImage": "Bild kann nicht abgerufen werden",
|
||||||
|
"imageFetchOverHops": "Bildabruf über {hops} Hops kann eine Weile dauern.",
|
||||||
|
"imageAlreadyBeingReceived": "Bild wird bereits empfangen",
|
||||||
|
"loadImage": "Bild laden",
|
||||||
|
"imageFetchFailedToSendRequest": "Bildabruf-Anfrage konnte nicht gesendet werden",
|
||||||
|
"imageUnavailable": "Bild derzeit nicht verfügbar",
|
||||||
|
"imageFetchTimedOut": "Bildabruf hat das Zeitlimit überschritten",
|
||||||
|
"imageReceiveCanceled": "Bildempfang abgebrochen",
|
||||||
|
"closeImagePreview": "Bildvorschau schließen",
|
||||||
|
"recordingReleaseToSend": "Aufnahme läuft ... loslassen zum Senden",
|
||||||
|
"sendingVoice": "Sprachnachricht wird gesendet ...",
|
||||||
|
"sendLongPressToRecordVoice": "Senden (lange drücken für Sprachaufnahme)",
|
||||||
|
"discardRecording": "Aufnahme verwerfen",
|
||||||
|
"hopCount": "{count, plural, =1{1 Hop} other{{count} Hops}}",
|
||||||
|
"channelsSectionDescription": "Broadcast-Kanäle für Mesh-Verkehr in der Nähe",
|
||||||
|
"roomsSectionDescription": "Gemeinsame Räume für laufende Teamkoordination",
|
||||||
|
"contactsSectionDescription": "Personen und Geräte, die Sie direkt erreichen können",
|
||||||
|
"noRecentChatters": "Keine kürzlichen Teilnehmer",
|
||||||
|
"public": "Öffentlich",
|
||||||
|
"failedToSendTicTacToeMove": "Tic-Tac-Toe-Zug konnte nicht gesendet werden",
|
||||||
|
"youWon": "Sie haben gewonnen",
|
||||||
|
"opponentWon": "Gegner hat gewonnen",
|
||||||
|
"gameDraw": "Unentschieden",
|
||||||
|
"yourTurn": "Sie sind am Zug",
|
||||||
|
"opponentTurn": "Gegner ist am Zug",
|
||||||
|
"pleaseEnterValidCoordinates": "Bitte geben Sie gültige Koordinaten ein",
|
||||||
|
"locationNotAvailable": "Standort nicht verfügbar",
|
||||||
|
"customCaveMapPoint": "Eigener Höhlenkarten-Punkt",
|
||||||
|
"mapPoint": "Kartenpunkt",
|
||||||
|
"fetchingMissingImageFragments": "Fehlende Fragmente werden abgerufen …",
|
||||||
|
"loadingImage": "Wird geladen …",
|
||||||
|
"receivingImage": "Wird empfangen …",
|
||||||
|
"tapToLoad": "Zum Laden tippen",
|
||||||
|
"connectDevice": "Gerät verbinden",
|
||||||
|
"chooseTransportSubtitle": "Wählen Sie Bluetooth, WiFi oder Seriell als Verbindung",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Netzwerk",
|
||||||
|
"serial": "Seriell",
|
||||||
|
"scan": "Scannen",
|
||||||
|
"pressScanToSearchForDevices": "Drücken Sie auf Scannen, um nach Geräten in der Nähe zu suchen",
|
||||||
|
"failedToConnectToDevice": "Verbindung mit {name} fehlgeschlagen",
|
||||||
|
"unknownDevice": "Unbekanntes Gerät",
|
||||||
|
"showingCachedResultsTapRefresh": "Zwischengespeicherte Ergebnisse werden angezeigt. Tippen Sie auf Aktualisieren, um erneut zu scannen.",
|
||||||
|
"scanningLocalNetworkOnPort": "Lokales Netzwerk wird nach MeshCore-WiFi-Geräten auf Port {port} durchsucht",
|
||||||
|
"cancelScanAndAddServer": "Scan abbrechen und Server hinzufügen",
|
||||||
|
"addServer": "Server hinzufügen",
|
||||||
|
"scanningProgressIps": "Scannen... {scanned}/{total} IPs",
|
||||||
|
"cancelAndEnterManually": "Abbrechen und manuell eingeben",
|
||||||
|
"noRecentOrDiscoveredServers": "Noch keine kürzlich verwendeten oder gefundenen Server",
|
||||||
|
"recentlyUsed": "Kürzlich verwendet",
|
||||||
|
"connecting": "Verbindung wird hergestellt...",
|
||||||
|
"discoveredOnThisNetwork": "In diesem Netzwerk gefunden",
|
||||||
|
"serverNoLongerAvailable": "Server unter {host}:{port} ist nicht mehr verfügbar. Bitte scannen Sie erneut, um aktive Server zu finden.",
|
||||||
|
"failedToConnectToHost": "Verbindung mit {host}:{port} fehlgeschlagen",
|
||||||
|
"enterValidIpAddress": "Geben Sie eine gültige IP-Adresse ein",
|
||||||
|
"enterValidTcpPort": "Geben Sie einen gültigen TCP-Port ein",
|
||||||
|
"ipAddress": "IP-Adresse",
|
||||||
|
"tcpPort": "TCP-Port",
|
||||||
|
"customServerPort": "Benutzerdefinierter Server-Port",
|
||||||
|
"chat": "Chat",
|
||||||
|
"restoringPreviousLink": "Vorherige Verbindung wird wiederhergestellt",
|
||||||
|
"noDeviceConnected": "Kein Gerät verbunden",
|
||||||
|
"sendAdvert": "Meinen Kontakt senden",
|
||||||
|
"advertMode": "Meinen Kontakt senden",
|
||||||
|
"advertModeSubtitle": "Wählen Sie, wie weit Ihre Kontaktdaten übertragen werden sollen.",
|
||||||
|
"floodAdvertSent": "Kontakt über das Mesh-Netzwerk gesendet",
|
||||||
|
"directAdvertSent": "Kontakt an Geräte in der Nähe gesendet"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Button to restart BLE scanning"
|
"description": "Button to restart BLE scanning"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Πατήστε για σύνδεση",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Subtitle text for device in scan list"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Η συσκευή δεν είναι συνδεδεμένη",
|
"deviceNotConnected": "Η συσκευή δεν είναι συνδεδεμένη",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Error message when device is not connected"
|
"description": "Error message when device is not connected"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Setting to show RX/TX indicators"
|
"description": "Setting to show RX/TX indicators"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Εμφάνιση ενδείξεων δραστηριότητας πακέτων στην επάνω μπάρα",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description for RX/TX indicators setting"
|
|
||||||
},
|
|
||||||
"disableMap": "Απενεργοποίηση χάρτη",
|
"disableMap": "Απενεργοποίηση χάρτη",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Setting to disable the map tab"
|
"description": "Setting to disable the map tab"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Απόκρυψη της καρτέλας χάρτη για μείωση κατανάλωσης μπαταρίας",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description for disable map setting"
|
|
||||||
},
|
|
||||||
"language": "Γλώσσα",
|
"language": "Γλώσσα",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Language setting label"
|
"description": "Language setting label"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "App version label"
|
"description": "App version label"
|
||||||
},
|
},
|
||||||
"appName": "Όνομα εφαρμογής",
|
|
||||||
"@appName": {
|
|
||||||
"description": "App name label"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Σχετικά με το MeshCore SAR",
|
"aboutMeshCoreSar": "Σχετικά με το MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "About dialog title"
|
"description": "About dialog title"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action to reset contact path for re-routing"
|
"description": "Action to reset contact path for re-routing"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Το δημόσιο κλειδί αντιγράφηκε στο πρόχειρο",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Success message when public key is copied"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "Το {label} αντιγράφηκε στο πρόχειρο",
|
"copiedToClipboard": "Το {label} αντιγράφηκε στο πρόχειρο",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Success message when a value is copied to clipboard",
|
"description": "Success message when a value is copied to clipboard",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Last seen label"
|
"description": "Last seen label"
|
||||||
},
|
},
|
||||||
"roomStatus": "Κατάσταση δωματίου",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Room status section header"
|
|
||||||
},
|
|
||||||
"loginStatus": "Κατάσταση σύνδεσης",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Login status label"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Μη συνδεδεμένος",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Not logged in status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Πρόσβαση διαχειριστή",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Admin access label"
|
|
||||||
},
|
|
||||||
"yes": "Ναι",
|
"yes": "Ναι",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Yes answer"
|
"description": "Yes answer"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Permissions label"
|
"description": "Permissions label"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Ο κωδικός αποθηκεύτηκε",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Password saved label"
|
|
||||||
},
|
|
||||||
"locationColon": "Τοποθεσία:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Location section header"
|
|
||||||
},
|
|
||||||
"telemetry": "Τηλεμετρία",
|
"telemetry": "Τηλεμετρία",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Telemetry section header"
|
"description": "Telemetry section header"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS from telemetry label"
|
"description": "GPS from telemetry label"
|
||||||
},
|
},
|
||||||
"updated": "Ενημερώθηκε",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Updated timestamp label"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Η διαδρομή για το {name} επαναφέρθηκε. Το επόμενο μήνυμα θα βρει νέα διαδρομή.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Info message after path reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Επανασύνδεση στο δωμάτιο",
|
"reLoginToRoom": "Επανασύνδεση στο δωμάτιο",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Button to re-login to room"
|
"description": "Button to re-login to room"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Label for radio frequency field"
|
"description": "Label for radio frequency field"
|
||||||
},
|
},
|
||||||
"frequencyExample": "π.χ. 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Helper text example for frequency"
|
|
||||||
},
|
|
||||||
"bandwidth": "Εύρος ζώνης",
|
"bandwidth": "Εύρος ζώνης",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Label for bandwidth dropdown"
|
"description": "Label for bandwidth dropdown"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
"contactTrails": "Ίχνη επαφών",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Contact trails section header"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Εμφάνιση όλων των ιχνών επαφών",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Toggle label to show all contact trails"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Δεν υπάρχουν επαφές με ιστορικό τοποθεσίας",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtitle when no contacts have trails"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Εμφανίζονται ίχνη για {count} επαφές",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtitle showing number of contacts with trails",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Μεμονωμένα ίχνη επαφών",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Expansion tile title for individual contact trails"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Πληροφορίες συσκευής",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Device information section header"
|
|
||||||
},
|
|
||||||
"bleName": "Όνομα BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Bluetooth Low Energy device name label"
|
|
||||||
},
|
|
||||||
"meshName": "Όνομα mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh network name label"
|
|
||||||
},
|
|
||||||
"notSet": "Δεν έχει οριστεί",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Label when a value is not set"
|
|
||||||
},
|
|
||||||
"model": "Μοντέλο",
|
"model": "Μοντέλο",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Device model label"
|
"description": "Device model label"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Version label"
|
"description": "Version label"
|
||||||
},
|
},
|
||||||
"buildDate": "Ημερομηνία build",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Firmware build date label"
|
|
||||||
},
|
|
||||||
"firmware": "Υλικολογισμικό",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Firmware label"
|
|
||||||
},
|
|
||||||
"maxContacts": "Μέγιστες επαφές",
|
"maxContacts": "Μέγιστες επαφές",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Maximum contacts capacity label"
|
"description": "Maximum contacts capacity label"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Public information section header"
|
"description": "Public information section header"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Όνομα δικτύου mesh",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh network name field label"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Όνομα που μεταδίδεται στις διαφημίσεις mesh",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Helper text for mesh network name field"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Τηλεμετρία και κοινοποίηση τοποθεσίας",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Telemetry and location sharing toggle label"
|
|
||||||
},
|
|
||||||
"lat": "Πλάτος",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Latitude field label (short form)"
|
|
||||||
},
|
|
||||||
"lon": "Μήκος",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Longitude field label (short form)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Χρήση τρέχουσας τοποθεσίας",
|
"useCurrentLocation": "Χρήση τρέχουσας τοποθεσίας",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip for use current location button"
|
"description": "Tooltip for use current location button"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "Το {label} αντιγράφηκε στο πρόχειρο",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Short success message when copying to clipboard",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Αποτυχία αποθήκευσης: {error}",
|
"failedToSave": "Αποτυχία αποθήκευσης: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Generic error message for save failures",
|
"description": "Generic error message for save failures",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "SAR templates menu title"
|
"description": "SAR templates menu title"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "Διαχείριση προτύπων SAR",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "Subtitle for SAR templates settings"
|
|
||||||
},
|
|
||||||
"addTemplate": "Προσθήκη προτύπου",
|
"addTemplate": "Προσθήκη προτύπου",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "Button to add new SAR template"
|
"description": "Button to add new SAR template"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "OK button label"
|
"description": "OK button label"
|
||||||
},
|
},
|
||||||
"permissionsSection": "Δικαιώματα",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "Permissions section header"
|
|
||||||
},
|
|
||||||
"locationPermission": "Άδεια τοποθεσίας",
|
"locationPermission": "Άδεια τοποθεσίας",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "Location permission label"
|
"description": "Location permission label"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "Welcome wizard first page title"
|
"description": "Welcome wizard first page title"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "Ένα ισχυρό εργαλείο επικοινωνίας εκτός δικτύου για επιχειρήσεις έρευνας και διάσωσης. Συνδεθείτε με την ομάδα σας χρησιμοποιώντας τεχνολογία mesh radio όταν τα παραδοσιακά δίκτυα δεν είναι διαθέσιμα.",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "Welcome wizard first page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "Σύνδεση στο radio σας",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "Wizard connecting page title"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "Συνδέστε το smartphone σας με μια συσκευή MeshCore radio μέσω Bluetooth για να αρχίσετε να επικοινωνείτε εκτός δικτύου.",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "Wizard connecting page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "Σάρωση για κοντινές συσκευές MeshCore",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "Wizard connecting feature 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "Σύζευξη με το radio σας μέσω Bluetooth",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "Wizard connecting feature 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "Λειτουργεί πλήρως εκτός σύνδεσης - δεν απαιτείται internet",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "Wizard connecting feature 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "Κανάλια",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "Wizard channel page title"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "Μεταδώστε μηνύματα σε όλους σε ένα κανάλι, ιδανικό για ανακοινώσεις και συντονισμό σε όλη την ομάδα.",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "Wizard channel page description"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "Δημόσιο κανάλι για γενική επικοινωνία ομάδας",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "Wizard channel feature 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "Δημιουργία προσαρμοσμένων καναλιών για συγκεκριμένες ομάδες",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "Wizard channel feature 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "Τα μηνύματα αναμεταδίδονται αυτόματα από το mesh",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "Wizard channel feature 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "Επαφές",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "Wizard contacts page title"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "Τα μέλη της ομάδας σας εμφανίζονται αυτόματα καθώς συνδέονται στο δίκτυο mesh. Στείλτε τους άμεσα μηνύματα ή δείτε την τοποθεσία τους.",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "Wizard contacts page description"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "Οι επαφές εντοπίζονται αυτόματα",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "Wizard contacts feature 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "Αποστολή ιδιωτικών άμεσων μηνυμάτων",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "Wizard contacts feature 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "Προβολή επιπέδου μπαταρίας και χρόνου τελευταίας εμφάνισης",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "Wizard contacts feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "Χάρτης και τοποθεσία",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "Wizard map page title"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "Παρακολουθήστε την ομάδα σας σε πραγματικό χρόνο και σημειώστε σημαντικές τοποθεσίες για επιχειρήσεις έρευνας και διάσωσης.",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "Wizard map page description"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "Δείκτες SAR για εντοπισμένα άτομα, φωτιές και σημεία συγκέντρωσης",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "Wizard map feature 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "Παρακολούθηση GPS των μελών της ομάδας σε πραγματικό χρόνο",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "Wizard map feature 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "Λήψη χαρτών εκτός σύνδεσης για απομακρυσμένες περιοχές",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "Wizard map feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "Σχεδίαση σχημάτων και κοινοποίηση τακτικών πληροφοριών",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "Wizard map feature 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "Προβολή οδηγού καλωσορίσματος",
|
"viewWelcomeTutorial": "Προβολή οδηγού καλωσορίσματος",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "Settings option to re-show welcome wizard"
|
"description": "Settings option to re-show welcome wizard"
|
||||||
@@ -2303,5 +2068,525 @@
|
|||||||
"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": "Ζωνοπερατό φίλτρο φωνής",
|
||||||
|
"active": "Ενεργό",
|
||||||
|
"addContact": "Προσθήκη επαφής",
|
||||||
|
"all": "Όλα",
|
||||||
|
"clearAllLabel": "Εκκαθάριση όλων",
|
||||||
|
"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": "Κοινοποίηση",
|
||||||
|
"trace": "Ίχνος",
|
||||||
|
"discovery": "Ανακάλυψη",
|
||||||
|
"discoverRepeaters": "Ανακάλυψη αναμεταδοτών",
|
||||||
|
"discoverSensors": "Ανακάλυψη αισθητήρων",
|
||||||
|
"repeaterDiscoverySent": "Αποστολή ανακάλυψης αναμεταδοτών",
|
||||||
|
"sensorDiscoverySent": "Αποστολή ανακάλυψης αισθητήρων",
|
||||||
|
"clearedPendingDiscoveries": "Εκκαθάριση εκκρεμών ανακαλύψεων.",
|
||||||
|
"autoDiscovery": "Αυτόματη ανακάλυψη",
|
||||||
|
"enableAutomaticAdding": "Ενεργοποίηση αυτόματης προσθήκης",
|
||||||
|
"autoaddRepeaters": "Αυτόματη προσθήκη αναμεταδοτών",
|
||||||
|
"autoaddRoomServers": "Αυτόματη προσθήκη διακομιστών δωματίου",
|
||||||
|
"autoaddSensors": "Αυτόματη προσθήκη αισθητήρων",
|
||||||
|
"autoaddUsers": "Αυτόματη προσθήκη χρηστών",
|
||||||
|
"overwriteOldestWhenFull": "Αντικατάσταση παλαιότερων όταν γεμίσει",
|
||||||
|
"storage": "Αποθήκευση",
|
||||||
|
"dangerZone": "Ζώνη κινδύνου",
|
||||||
|
"profiles": "Προφίλ",
|
||||||
|
"favourites": "Αγαπημένα",
|
||||||
|
"sensors": "Αισθητήρες",
|
||||||
|
"others": "Άλλοι",
|
||||||
|
"gpsModule": "Μονάδα GPS",
|
||||||
|
"liveTraffic": "Ζωντανή κίνηση",
|
||||||
|
"repeatersMap": "Χάρτης αναμεταδοτών",
|
||||||
|
"blePacketLogs": "Αρχεία πακέτων BLE",
|
||||||
|
"onlineTraceDatabase": "Βάση δεδομένων ιχνών",
|
||||||
|
"routePathByteSize": "Μέγεθος διαδρομής σε bytes",
|
||||||
|
"messageNotifications": "Ειδοποιήσεις μηνυμάτων",
|
||||||
|
"sarAlerts": "Ειδοποιήσεις SAR",
|
||||||
|
"discoveryNotifications": "Ειδοποιήσεις ανακάλυψης",
|
||||||
|
"updateNotifications": "Ειδοποιήσεις ενημερώσεων",
|
||||||
|
"muteWhileAppIsOpen": "Σίγαση με ανοιχτή εφαρμογή",
|
||||||
|
"disableContacts": "Απενεργοποίηση επαφών",
|
||||||
|
"enableSensorsTab": "Ενεργοποίηση καρτέλας Αισθητήρων",
|
||||||
|
"enableProfiles": "Ενεργοποίηση προφίλ",
|
||||||
|
"nearestRepeaterFallback": "Πλησιέστερος αναμεταδότης ως εφεδρεία",
|
||||||
|
"deleteAllStoredMessageHistory": "Διαγραφή όλου του ιστορικού",
|
||||||
|
"messageFontSize": "Μέγεθος γραμματοσειράς μηνυμάτων",
|
||||||
|
"rotateMapWithHeading": "Περιστροφή χάρτη με κατεύθυνση",
|
||||||
|
"showMapDebugInfo": "Εμφάνιση πληροφοριών αποσφαλμάτωσης",
|
||||||
|
"openMapInFullscreen": "Άνοιγμα χάρτη σε πλήρη οθόνη",
|
||||||
|
"showSarMarkersLabel": "Εμφάνιση δεικτών SAR",
|
||||||
|
"hideRepeatersOnMap": "Απόκρυψη αναμεταδοτών στον χάρτη",
|
||||||
|
"setMapScale": "Ρύθμιση κλίμακας χάρτη",
|
||||||
|
"customMapScaleSaved": "Προσαρμοσμένη κλίμακα χάρτη αποθηκεύτηκε",
|
||||||
|
"voiceBitrate": "Ρυθμός bit φωνής",
|
||||||
|
"voiceCompressor": "Συμπιεστής φωνής",
|
||||||
|
"voiceLimiter": "Περιοριστής φωνής",
|
||||||
|
"micAutoGain": "Αυτόματη ενίσχυση μικροφώνου",
|
||||||
|
"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": "Ήσυχο",
|
||||||
|
"busy": "Απασχολημένο",
|
||||||
|
"searchMessages": "Αναζήτηση μηνυμάτων",
|
||||||
|
"sendImageFromGallery": "Αποστολή εικόνας από τη γκαλερί",
|
||||||
|
"takePhoto": "Λήψη φωτογραφίας",
|
||||||
|
"allMessages": "Όλα τα μηνύματα",
|
||||||
|
"sendToPublicChannel": "Αποστολή στο δημόσιο κανάλι;",
|
||||||
|
"selectMarkerTypeAndDestination": "Επιλέξτε τύπο δείκτη και προορισμό",
|
||||||
|
"noDestinationsAvailableLabel": "Δεν υπάρχουν διαθέσιμοι προορισμοί",
|
||||||
|
"image": "Εικόνα",
|
||||||
|
"format": "Μορφή",
|
||||||
|
"dimensions": "Διαστάσεις",
|
||||||
|
"segments": "Τμήματα",
|
||||||
|
"transfers": "Μεταφορές",
|
||||||
|
"downloadedBy": "Λήφθηκε από",
|
||||||
|
"saveDiscoverySettings": "Αποθήκευση ρυθμίσεων ανακάλυψης",
|
||||||
|
"savePublicInfo": "Αποθήκευση δημόσιων πληροφοριών",
|
||||||
|
"saveRadioSettings": "Αποθήκευση ρυθμίσεων ραδιοφώνου",
|
||||||
|
"savePath": "Αποθήκευση διαδρομής",
|
||||||
|
"wipeDeviceData": "Διαγραφή δεδομένων συσκευής",
|
||||||
|
"wipeDevice": "Διαγραφή συσκευής",
|
||||||
|
"destructiveDeviceActions": "Καταστροφικές ενέργειες συσκευής.",
|
||||||
|
"chooseAPresetOrFinetuneCustomRadioSettings": "Επιλέξτε μια προεπιλογή ή ρυθμίστε τις ρυθμίσεις ραδιοφώνου.",
|
||||||
|
"used": "Χρησιμοποιήθηκε",
|
||||||
|
"total": "Σύνολο",
|
||||||
|
"renameValue": "Μετονομασία τιμής",
|
||||||
|
"customizeFields": "Προσαρμογή πεδίων",
|
||||||
|
"livePreview": "Ζωντανή προεπισκόπηση",
|
||||||
|
"refreshSchedule": "Χρονοδιάγραμμα ανανέωσης",
|
||||||
|
"noResponse": "Χωρίς απάντηση",
|
||||||
|
"refreshing": "Ανανέωση",
|
||||||
|
"unavailable": "Μη διαθέσιμο",
|
||||||
|
"pickARelayOrNodeToWatchInSensors": "Επιλέξτε ένα relay ή κόμβο για παρακολούθηση.",
|
||||||
|
"publicKeyLabel": "Δημόσιο κλειδί",
|
||||||
|
"alreadyInContacts": "Ήδη στις επαφές",
|
||||||
|
"connectToADeviceBeforeAddingContacts": "Συνδεθείτε σε μια συσκευή πριν προσθέσετε επαφές",
|
||||||
|
"fromContacts": "Από τις επαφές",
|
||||||
|
"onlineOnly": "Μόνο συνδεδεμένοι",
|
||||||
|
"inBoth": "Σε αμφότερα",
|
||||||
|
"source": "Πηγή",
|
||||||
|
"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": "Δεν υπάρχουν επαφές συσκευής.",
|
||||||
|
"autoSend": "Αυτόματη αποστολή",
|
||||||
|
"autoSendDescription": "Αποστολή μέσω τρέχουσας διαδρομής.",
|
||||||
|
"sendDirect": "Άμεση αποστολή",
|
||||||
|
"sendDirectDescription": "Αποστολή απευθείας σε αυτήν την επαφή.",
|
||||||
|
"sendFlood": "Αποστολή πλημμύρας",
|
||||||
|
"sendFloodDescription": "Αποστολή μέσω όλων των αναμεταδοτών.",
|
||||||
|
"postConnectDiscoveryTitle": "Ανακάλυψη αναμεταδοτών τώρα;",
|
||||||
|
"postConnectDiscoveryDescription": "Εκτελέστε ανακάλυψη αναμεταδοτών αμέσως μετά τη σύνδεση, ώστε να μπορείτε να δείτε κοντινούς κόμβους MeshCore και να τους προσθέσετε στο δίκτυό σας πιο γρήγορα.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Προσθήκη",
|
||||||
|
"addAdditionalDetails": "Προσθήκη επιπλέον λεπτομερειών",
|
||||||
|
"addPeer": "Προσθήκη κόμβου",
|
||||||
|
"addPeerManually": "Χειροκίνητη προσθήκη κόμβου",
|
||||||
|
"addedToContacts": "Ο/Η {name} προστέθηκε στις επαφές",
|
||||||
|
"addedToSensors": "Ο/Η {name} προστέθηκε στους αισθητήρες",
|
||||||
|
"allowAll": "Επιτρέπονται όλα",
|
||||||
|
"anonymousRxStats": "Ανώνυμα στατιστικά λήψης",
|
||||||
|
"appearance": "Εμφάνιση",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Βασική τηλεμετρία",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Ειδοποιήσεις βομβητή",
|
||||||
|
"cancelImageReceive": "Ακύρωση λήψης εικόνας",
|
||||||
|
"choose": "Επιλογή",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Επιλέξτε ποιες καρτέλες και ενότητες επαφών θα εμφανίζονται",
|
||||||
|
"clearCache": "Εκκαθάριση προσωρινής μνήμης",
|
||||||
|
"clearHistory": "Εκκαθάριση ιστορικού",
|
||||||
|
"clearLiveView": "Εκκαθάριση ζωντανής προβολής",
|
||||||
|
"clearLogs": "Εκκαθάριση αρχείων καταγραφής",
|
||||||
|
"clearOfflineCache": "Εκκαθάριση προσωρινής μνήμης εκτός σύνδεσης;",
|
||||||
|
"clearOverlay": "Εκκαθάριση επικάλυψης",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Εκκαθάριση διαδρομής μόνο μετά την αποτυχία όλων των επαναπροσπαθειών",
|
||||||
|
"codecBitrateAndAudioProcessing": "Κωδικοποιητής, ρυθμός bit και επεξεργασία ήχου",
|
||||||
|
"connectByIpAddress": "Σύνδεση μέσω διεύθυνσης IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Ελέγξτε ποιες ειδοποιήσεις λαμβάνετε",
|
||||||
|
"copyFullHex": "Αντιγραφή πλήρους Hex",
|
||||||
|
"copyRawResponse": "Αντιγραφή ακατέργαστης απόκρισης",
|
||||||
|
"couldNotLoadMetHistory": "Αδυναμία φόρτωσης ιστορικού MET",
|
||||||
|
"customizeSensor": "Προσαρμογή {name}",
|
||||||
|
"data": "Δεδομένα",
|
||||||
|
"deleteGroup": "Διαγραφή ομάδας",
|
||||||
|
"deleteStyleConfirm": "Διαγραφή {name};",
|
||||||
|
"deleteThisDrawing": "Διαγραφή αυτού του {type};",
|
||||||
|
"deny": "Απόρριψη",
|
||||||
|
"deviceInfo": "Πληροφορίες συσκευής",
|
||||||
|
"deviceSettings": "Ρυθμίσεις συσκευής",
|
||||||
|
"deviceTimeSynced": "Ο χρόνος της συσκευής συγχρονίστηκε με αυτό το τηλέφωνο.",
|
||||||
|
"disableFastGpsPublishing": "Απενεργοποίηση γρήγορης δημοσίευσης GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Εμφάνιση, δείκτες και βάση δεδομένων ιχνών",
|
||||||
|
"download": "Λήψη",
|
||||||
|
"environmentalTelemetry": "Περιβαλλοντική τηλεμετρία",
|
||||||
|
"errorCheckingForUpdates": "Σφάλμα ελέγχου ενημερώσεων: {error}",
|
||||||
|
"errorGeneric": "Σφάλμα: {error}",
|
||||||
|
"errorOpeningDownload": "Σφάλμα ανοίγματος λήψης: {error}",
|
||||||
|
"exportLogs": "Εξαγωγή αρχείων καταγραφής",
|
||||||
|
"extraAggressiveAvifCompression": "Εξαιρετικά επιθετική συμπίεση AVIF",
|
||||||
|
"failedToAddContact": "Αποτυχία προσθήκης επαφής: {error}",
|
||||||
|
"failedToAddName": "Αποτυχία προσθήκης {name}: {error}",
|
||||||
|
"failedToClearChannels": "Αποτυχία εκκαθάρισης καναλιών: {error}",
|
||||||
|
"failedToClearContacts": "Αποτυχία εκκαθάρισης επαφών: {error}",
|
||||||
|
"failedToConnectViaSerial": "Αποτυχία σύνδεσης μέσω σειριακής θύρας",
|
||||||
|
"failedToLoadPreviewImage": "Αποτυχία φόρτωσης εικόνας προεπισκόπησης: {error}",
|
||||||
|
"failedToLoadTrace": "Αποτυχία φόρτωσης ίχνους: {error}",
|
||||||
|
"failedToSendSarMarker": "Αποτυχία αποστολής δείκτη SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Αποτυχία ρύθμισης λειτουργίας βομβητή: {error}",
|
||||||
|
"failedToSetGpsMode": "Αποτυχία ρύθμισης λειτουργίας GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Αποτυχία συγχρονισμού χρόνου συσκευής: {error}",
|
||||||
|
"failedToWipeDeviceData": "Αποτυχία διαγραφής δεδομένων συσκευής: {error}",
|
||||||
|
"fastGpsTargetChannel": "Κανάλι-στόχος γρήγορου GPS",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Γρήγορες ενημερώσεις, όρια και δικαιώματα",
|
||||||
|
"fetchingCatalog": "Ανάκτηση καταλόγου...",
|
||||||
|
"fifteenMinutes": "15 λεπτά",
|
||||||
|
"fifteenSeconds": "15 δευτερόλεπτα",
|
||||||
|
"filterByDirection": "Φιλτράρισμα κατά κατεύθυνση",
|
||||||
|
"finish": "Τέλος",
|
||||||
|
"fiveMinutes": "5 λεπτά",
|
||||||
|
"fiveSeconds": "5 δευτερόλεπτα",
|
||||||
|
"fix": "Στίξη",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Πολιτική διαφήμισης GPS",
|
||||||
|
"gpsAndLocation": "GPS & Τοποθεσία",
|
||||||
|
"gpsDiagnostics": "Διαγνωστικά GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Βοήθεια",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Κρυφό",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Πώς το ραδιόφωνο προσθέτει αυτόματα ανακαλυφθέντες κόμβους.",
|
||||||
|
"images": "Εικόνες",
|
||||||
|
"importProfile": "Εισαγωγή προφίλ",
|
||||||
|
"lastFix": "Τελευταία στίξη",
|
||||||
|
"latest": "Πιο πρόσφατο",
|
||||||
|
"latitude": "Γεωγραφικό πλάτος",
|
||||||
|
"loadASavedRegion": "Φόρτωση αποθηκευμένης περιοχής",
|
||||||
|
"loading": "Φόρτωση",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Κοινοποίηση τοποθεσίας, υλικό και διάστημα ενημέρωσης.",
|
||||||
|
"locationTelemetry": "Τηλεμετρία τοποθεσίας",
|
||||||
|
"lockDestination": "Κλείδωμα προορισμού",
|
||||||
|
"longitude": "Γεωγραφικό μήκος",
|
||||||
|
"mapStyle": "Στυλ χάρτη",
|
||||||
|
"max": "Μέγ.",
|
||||||
|
"maxZoom": "Μέγ. ζουμ",
|
||||||
|
"messaging": "Μηνύματα",
|
||||||
|
"metHistory": "Ιστορικό MET",
|
||||||
|
"min": "Ελάχ.",
|
||||||
|
"minZoom": "Ελάχ. ζουμ",
|
||||||
|
"moveDown": "Μετακίνηση κάτω",
|
||||||
|
"moveUp": "Μετακίνηση πάνω",
|
||||||
|
"multiAckMode": "Λειτουργία πολλαπλού ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Διαχείριση χώρου εργασίας πολλαπλών συσκευών",
|
||||||
|
"myLocation": "Η τοποθεσία μου",
|
||||||
|
"nameAndTelemetryShared": "Το όνομα και η τηλεμετρία κοινοποιούνται σε άλλες συσκευές.",
|
||||||
|
"newVersionAvailable": "Νέα έκδοση: {version}",
|
||||||
|
"newer": "Νεότερο",
|
||||||
|
"noNeighboursFound": "Δεν βρέθηκαν γείτονες",
|
||||||
|
"noServersFound": "Δεν βρέθηκαν διακομιστές",
|
||||||
|
"notSet": "Δεν έχει οριστεί",
|
||||||
|
"notifications": "Ειδοποιήσεις",
|
||||||
|
"offZeroSeconds": "Ανενεργό (0 δ)",
|
||||||
|
"offlineMaps": "Χάρτες εκτός σύνδεσης",
|
||||||
|
"older": "Παλαιότερο",
|
||||||
|
"oneByteMode0": "1 byte (λειτουργία 0)",
|
||||||
|
"oneHour": "1 ώρα",
|
||||||
|
"oneMinute": "1 λεπτό",
|
||||||
|
"openPacketLogs": "Άνοιγμα αρχείων πακέτων",
|
||||||
|
"packetTypeHelp": "Βοήθεια τύπων πακέτων",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Επικολλήστε σύνδεσμο κοινοποίησης ή hex διαφήμιση",
|
||||||
|
"pathHash": "Hash διαδρομής",
|
||||||
|
"pathSize": "Μέγεθος διαδρομής",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Πολύγωνο",
|
||||||
|
"rawResponseCopied": "Η ακατέργαστη απόκριση αντιγράφηκε",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Επαναποστολή μέσω πλησιέστερου αναμεταδότη σε αποτυχία",
|
||||||
|
"resolveContact": "Επίλυση επαφής",
|
||||||
|
"routingRetriesAndDestinationLock": "Δρομολόγηση, επαναπροσπάθειες και κλείδωμα προορισμού",
|
||||||
|
"samples": "Δείγματα",
|
||||||
|
"satellites": "Δορυφόροι",
|
||||||
|
"saving": "Αποθήκευση...",
|
||||||
|
"search": "Αναζήτηση",
|
||||||
|
"searchByNameOrToken": "Αναζήτηση με όνομα ή token",
|
||||||
|
"searchDiscoveredNodes": "Αναζήτηση ανακαλυφθέντων κόμβων",
|
||||||
|
"searchInCurrentFilter": "Αναζήτηση στο τρέχον φίλτρο",
|
||||||
|
"searchLogs": "Αναζήτηση αρχείων καταγραφής...",
|
||||||
|
"searchSensors": "Αναζήτηση αισθητήρων",
|
||||||
|
"searchThisSection": "Αναζήτηση σε αυτήν την ενότητα",
|
||||||
|
"selectedRelays": "Επιλεγμένοι αναμεταδότες",
|
||||||
|
"sensor": "Αισθητήρας",
|
||||||
|
"serialError": "Σφάλμα σειριακής: {error}",
|
||||||
|
"setPathForContact": "Ορισμός διαδρομής για {name}",
|
||||||
|
"shareLiveGps": "Κοινοποίηση ζωντανού GPS",
|
||||||
|
"shareMyTiles": "Κοινοποίηση πλακιδίων μου",
|
||||||
|
"sharingLocationFromPhone": "Κοινοποίηση τοποθεσίας σε αυτό το κανάλι από το τηλέφωνο.",
|
||||||
|
"sharingLocationFromRadio": "Κοινοποίηση τοποθεσίας σε αυτό το κανάλι από το ραδιόφωνο.",
|
||||||
|
"signalDbm": "Σήμα {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Μέγεθος, συμπίεση και προεπισκόπηση",
|
||||||
|
"skip": "Παράλειψη",
|
||||||
|
"smallerFileSize": "Μικρότερο μέγεθος αρχείου",
|
||||||
|
"sort": "Ταξινόμηση",
|
||||||
|
"stoppedSharingLocation": "Σταμάτησε η κοινοποίηση τοποθεσίας σε αυτό το κανάλι.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Αποσιώπηση ειδοποιήσεων όταν η εφαρμογή είναι στο προσκήνιο",
|
||||||
|
"tabsAndNavigation": "Καρτέλες & Πλοήγηση",
|
||||||
|
"templatesAndTutorials": "Πρότυπα και οδηγίες",
|
||||||
|
"tenMinutes": "10 λεπτά",
|
||||||
|
"tenSeconds": "10 δευτερόλεπτα",
|
||||||
|
"testSendUpdate": "Δοκιμαστική αποστολή ενημέρωσης",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Θέμα, γλώσσα και προτιμήσεις εμφάνισης",
|
||||||
|
"thirtyMinutes": "30 λεπτά",
|
||||||
|
"thirtySeconds": "30 δευτερόλεπτα",
|
||||||
|
"threeBytesMode2": "3 bytes (λειτουργία 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Στατιστικά κίνησης, ιστορικό μηνυμάτων και εργαλεία προγραμματιστή",
|
||||||
|
"twoBytesMode1": "2 bytes (λειτουργία 1)",
|
||||||
|
"twoMinutes": "2 λεπτά",
|
||||||
|
"undo": "Αναίρεση",
|
||||||
|
"updated": "Ενημερώθηκε",
|
||||||
|
"useContactFlags": "Χρήση σημαιών επαφής",
|
||||||
|
"useSavedCoordinates": "Χρήση αποθηκευμένων συντεταγμένων",
|
||||||
|
"view": "Προβολή",
|
||||||
|
"viewNeighbours": "Προβολή γειτόνων",
|
||||||
|
"viewPublicStats": "Προβολή δημόσιων στατιστικών",
|
||||||
|
"setRegionScope": "Ορισμός εύρους περιοχής",
|
||||||
|
"regionScope": "Εύρος περιοχής",
|
||||||
|
"regionScopeNone": "Κανένα (καθολικό)",
|
||||||
|
"clearRegionScope": "Εκκαθάριση εύρους",
|
||||||
|
"regionScopeWarning": "Μόνο οι αναμεταδότες που επιτρέπουν αυτή την περιοχή θα προωθήσουν.",
|
||||||
|
"discoverRegions": "Ανακάλυψη από αναμεταδότες",
|
||||||
|
"discoveringRegions": "Ανακάλυψη περιοχών...",
|
||||||
|
"enterRegionName": "Όνομα περιοχής (π.χ. auckland)",
|
||||||
|
"noRegionsFound": "Δεν βρέθηκαν περιοχές σε αυτόν τον αναμεταδότη.",
|
||||||
|
"regionScopeSet": "Εύρος περιοχής ορίστηκε σε {name}",
|
||||||
|
"regionScopeCleared": "Εύρος περιοχής εκκαθαρίστηκε",
|
||||||
|
"simpleMode": "Απλή λειτουργία",
|
||||||
|
"simpleModeDescription": "Μόνο συνομιλία και χάρτης με αγαπημένες επαφές",
|
||||||
|
"simpleModeRequiresLockedDestination": "Ενεργοποιήστε πρώτα το «Κλείδωμα προορισμού» με ένα κανάλι για να χρησιμοποιήσετε την Απλή λειτουργία",
|
||||||
|
"noFavouriteContacts": "Καμία αγαπημένη επαφή",
|
||||||
|
"selectDestinationFirst": "Επιλέξτε πρώτα κανάλι, επαφή ή δωμάτιο",
|
||||||
|
"recipientNoLongerAvailable": "Ο παραλήπτης δεν είναι πλέον διαθέσιμος",
|
||||||
|
"failedToSendMessage": "Αποτυχία αποστολής: {error}",
|
||||||
|
"tictactoeDirectOnly": "Η τρίλιζα λειτουργεί μόνο σε άμεσα μηνύματα. Επιλέξτε πρώτα μια επαφή.",
|
||||||
|
"deviceKeyUnavailable": "Το κλειδί της συσκευής δεν είναι διαθέσιμο",
|
||||||
|
"imageCompressionFailed": "Αποτυχία συμπίεσης εικόνας",
|
||||||
|
"imageFragmentationFailed": "Αποτυχία κατακερματισμού εικόνας",
|
||||||
|
"failedToAnnounceImage": "Αποτυχία ανακοίνωσης εικόνας",
|
||||||
|
"noRecipientSelected": "Δεν επιλέχθηκε παραλήπτης",
|
||||||
|
"imageSendFailed": "Αποτυχία αποστολής εικόνας",
|
||||||
|
"microphonePermissionRequiredForVoice": "Απαιτείται άδεια μικροφώνου για φωνητικά μηνύματα",
|
||||||
|
"recordingDiscarded": "Η ηχογράφηση απορρίφθηκε",
|
||||||
|
"publicChannelMediaWarning": "Πρόκειται να στείλετε {mediaType} στο δημόσιο κανάλι. Δεν συνιστάται, καθώς μπορεί να το λάβουν όλοι στο δίκτυο mesh. Επιλέξτε ιδιωτικό ή επισημασμένο κανάλι, εκτός αν αυτό θέλετε.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Επιλέξτε ιδιωτικό κανάλι για να μοιραστείτε την τοποθεσία σας.",
|
||||||
|
"stopSharingMyLocation": "Διακοπή κοινοποίησης της τοποθεσίας μου",
|
||||||
|
"shareMyLocation": "Κοινοποίηση της τοποθεσίας μου",
|
||||||
|
"stopRecording": "Διακοπή ηχογράφησης",
|
||||||
|
"recordVoice": "Ηχογράφηση φωνητικού μηνύματος",
|
||||||
|
"moreActions": "Περισσότερες ενέργειες",
|
||||||
|
"noMessagesInFilter": "Δεν υπάρχουν μηνύματα σε αυτό το φίλτρο",
|
||||||
|
"noMatchesInFilter": "Δεν υπάρχουν αποτελέσματα σε αυτό το φίλτρο",
|
||||||
|
"syncFailed": "Αποτυχία συγχρονισμού: {error}",
|
||||||
|
"invalidLink": "Μη έγκυρος σύνδεσμος",
|
||||||
|
"cannotOpenLink": "Δεν είναι δυνατό το άνοιγμα του συνδέσμου",
|
||||||
|
"failedToOpenLink": "Αποτυχία ανοίγματος του συνδέσμου",
|
||||||
|
"failedToResendMessage": "Αποτυχία επαναποστολής του μηνύματος",
|
||||||
|
"retryFailed": "Η επανάληψη απέτυχε: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Διαγραφή σχεδίου και μηνύματος",
|
||||||
|
"lastEchoRelay": "Τελευταία ηχώ: αναμεταδότης",
|
||||||
|
"lastEchoPath": "Τελευταία ηχώ: διαδρομή",
|
||||||
|
"lastEchoBytesReport": "Τελευταία ηχώ: αναφορά byte",
|
||||||
|
"noGpsCoordinatesAvailable": "Δεν υπάρχουν διαθέσιμες συντεταγμένες GPS",
|
||||||
|
"notASarMarker": "Δεν είναι δείκτης SAR",
|
||||||
|
"failedToSaveTemplate": "Αποτυχία αποθήκευσης προτύπου: {error}",
|
||||||
|
"drawingNotFound": "Το σχέδιο δεν βρέθηκε",
|
||||||
|
"unknownDrawingType": "Άγνωστος τύπος σχεδίου",
|
||||||
|
"customMapMarker": "Προσαρμοσμένος δείκτης χάρτη",
|
||||||
|
"customMapPointLabel": "Σημείο: {point}",
|
||||||
|
"mapIdLabel": "ID χάρτη: {id}",
|
||||||
|
"cannotFetchVoice": "Δεν είναι δυνατή η λήψη της φωνής",
|
||||||
|
"senderContactUnknown": "Η επαφή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα τις επαφές.",
|
||||||
|
"senderRouteUnknown": "Η διαδρομή του αποστολέα είναι άγνωστη. Συγχρονίστε πρώτα επαφές/διαδρομή.",
|
||||||
|
"messageTooFar": "Το μήνυμα είναι πολύ μακριά ({hops} αναπηδήσεις, μέγ. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Η διαδρομή του αποστολέα δεν απάντησε στον έλεγχο διαδρομής. Συγχρονίστε επαφές/διαδρομή και δοκιμάστε ξανά.",
|
||||||
|
"senderRouteNoRawResponse": "Η διαδρομή του αποστολέα δεν απάντησε στη raw διαδρομή μεταφοράς.",
|
||||||
|
"voiceFetchOverHops": "Η λήψη φωνής μέσω {hops} αναπηδήσεων μπορεί να διαρκέσει.",
|
||||||
|
"voiceReceiveCanceled": "Η λήψη φωνής ακυρώθηκε",
|
||||||
|
"fetchingMissingVoiceFragments": "Λήψη φωνητικών τμημάτων που λείπουν",
|
||||||
|
"receivingVoice": "Λήψη φωνής",
|
||||||
|
"cannotFetchImage": "Δεν είναι δυνατή η λήψη της εικόνας",
|
||||||
|
"imageFetchOverHops": "Η λήψη εικόνας μέσω {hops} αναπηδήσεων μπορεί να διαρκέσει.",
|
||||||
|
"imageAlreadyBeingReceived": "Η εικόνα λαμβάνεται ήδη",
|
||||||
|
"loadImage": "Φόρτωση εικόνας",
|
||||||
|
"imageFetchFailedToSendRequest": "Αποτυχία αποστολής αιτήματος εικόνας",
|
||||||
|
"imageUnavailable": "Η εικόνα δεν είναι διαθέσιμη αυτήν τη στιγμή",
|
||||||
|
"imageFetchTimedOut": "Η λήψη της εικόνας έληξε",
|
||||||
|
"imageReceiveCanceled": "Η λήψη της εικόνας ακυρώθηκε",
|
||||||
|
"closeImagePreview": "Κλείσιμο προεπισκόπησης εικόνας",
|
||||||
|
"recordingReleaseToSend": "Ηχογράφηση... αφήστε για αποστολή",
|
||||||
|
"sendingVoice": "Αποστολή φωνής...",
|
||||||
|
"sendLongPressToRecordVoice": "Αποστολή (παρατεταμένο πάτημα για ηχογράφηση)",
|
||||||
|
"discardRecording": "Απόρριψη ηχογράφησης",
|
||||||
|
"hopCount": "{count, plural, one{{count} αναπήδηση} other{{count} αναπηδήσεις}}",
|
||||||
|
"channelsSectionDescription": "Κανάλια εκπομπής για κοντινή κίνηση mesh",
|
||||||
|
"roomsSectionDescription": "Κοινόχρηστοι χώροι για συνεχή συντονισμό ομάδας",
|
||||||
|
"contactsSectionDescription": "Άτομα και συσκευές που μπορείτε να προσεγγίσετε άμεσα",
|
||||||
|
"noRecentChatters": "Κανένας πρόσφατος συνομιλητής",
|
||||||
|
"public": "Δημόσιο",
|
||||||
|
"failedToSendTicTacToeMove": "Αποτυχία αποστολής κίνησης τρίλιζας",
|
||||||
|
"youWon": "Κερδίσατε",
|
||||||
|
"opponentWon": "Κέρδισε ο αντίπαλος",
|
||||||
|
"gameDraw": "Ισοπαλία",
|
||||||
|
"yourTurn": "Η σειρά σας",
|
||||||
|
"opponentTurn": "Σειρά του αντιπάλου",
|
||||||
|
"pleaseEnterValidCoordinates": "Εισαγάγετε έγκυρες συντεταγμένες",
|
||||||
|
"locationNotAvailable": "Η τοποθεσία δεν είναι διαθέσιμη",
|
||||||
|
"customCaveMapPoint": "Προσαρμοσμένο σημείο χάρτη σπηλαίου",
|
||||||
|
"mapPoint": "Σημείο χάρτη",
|
||||||
|
"fetchingMissingImageFragments": "Λήψη τμημάτων που λείπουν…",
|
||||||
|
"loadingImage": "Φόρτωση…",
|
||||||
|
"receivingImage": "Λήψη…",
|
||||||
|
"tapToLoad": "Πατήστε για φόρτωση",
|
||||||
|
"connectDevice": "Σύνδεση συσκευής",
|
||||||
|
"chooseTransportSubtitle": "Επιλέξτε μεταφορά Bluetooth, WiFi ή σειριακή",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Δίκτυο",
|
||||||
|
"serial": "Σειριακή θύρα",
|
||||||
|
"scan": "Σάρωση",
|
||||||
|
"pressScanToSearchForDevices": "Πατήστε «Σάρωση» για αναζήτηση κοντινών συσκευών",
|
||||||
|
"failedToConnectToDevice": "Αποτυχία σύνδεσης με {name}",
|
||||||
|
"unknownDevice": "Άγνωστη συσκευή",
|
||||||
|
"showingCachedResultsTapRefresh": "Εμφανίζονται αποτελέσματα από την προσωρινή μνήμη. Πατήστε ανανέωση για νέα σάρωση.",
|
||||||
|
"scanningLocalNetworkOnPort": "Σάρωση τοπικού δικτύου για συσκευές MeshCore WiFi στη θύρα {port}",
|
||||||
|
"cancelScanAndAddServer": "Ακύρωση σάρωσης και προσθήκη διακομιστή",
|
||||||
|
"addServer": "Προσθήκη διακομιστή",
|
||||||
|
"scanningProgressIps": "Σάρωση... {scanned}/{total} διευθύνσεις IP",
|
||||||
|
"cancelAndEnterManually": "Ακύρωση και χειροκίνητη εισαγωγή",
|
||||||
|
"noRecentOrDiscoveredServers": "Δεν υπάρχουν ακόμη πρόσφατοι ή εντοπισμένοι διακομιστές",
|
||||||
|
"recentlyUsed": "Πρόσφατα χρησιμοποιημένοι",
|
||||||
|
"connecting": "Γίνεται σύνδεση...",
|
||||||
|
"discoveredOnThisNetwork": "Εντοπίστηκαν σε αυτό το δίκτυο",
|
||||||
|
"serverNoLongerAvailable": "Ο διακομιστής στο {host}:{port} δεν είναι πλέον διαθέσιμος. Σαρώστε ξανά για να βρείτε ενεργούς διακομιστές.",
|
||||||
|
"failedToConnectToHost": "Αποτυχία σύνδεσης με {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Εισαγάγετε έγκυρη διεύθυνση IP",
|
||||||
|
"enterValidTcpPort": "Εισαγάγετε έγκυρη θύρα TCP",
|
||||||
|
"ipAddress": "Διεύθυνση IP",
|
||||||
|
"tcpPort": "Θύρα TCP",
|
||||||
|
"customServerPort": "Προσαρμοσμένη θύρα διακομιστή",
|
||||||
|
"chat": "Συνομιλία",
|
||||||
|
"restoringPreviousLink": "Επαναφορά προηγούμενης σύνδεσης",
|
||||||
|
"noDeviceConnected": "Δεν υπάρχει συνδεδεμένη συσκευή",
|
||||||
|
"sendAdvert": "Αποστολή της επαφής μου",
|
||||||
|
"advertMode": "Αποστολή της επαφής μου",
|
||||||
|
"advertModeSubtitle": "Επιλέξτε πόσο μακριά θα φτάσουν τα στοιχεία επαφής σας.",
|
||||||
|
"floodAdvertSent": "Η επαφή στάλθηκε σε όλο το δίκτυο mesh",
|
||||||
|
"directAdvertSent": "Η επαφή στάλθηκε σε κοντινές συσκευές"
|
||||||
}
|
}
|
||||||
|
|||||||
1491
lib/l10n/app_en.arb
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Botón para reiniciar el escaneo BLE"
|
"description": "Botón para reiniciar el escaneo BLE"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Toca para conectar",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Texto de subtítulo para dispositivo en la lista de escaneo"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Dispositivo no conectado",
|
"deviceNotConnected": "Dispositivo no conectado",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Mensaje de error cuando el dispositivo no está conectado"
|
"description": "Mensaje de error cuando el dispositivo no está conectado"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Configuración para mostrar indicadores RX/TX"
|
"description": "Configuración para mostrar indicadores RX/TX"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Mostrar indicadores de actividad de paquetes en la barra superior",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Descripción de la configuración de indicadores RX/TX"
|
|
||||||
},
|
|
||||||
"disableMap": "Desactivar mapa",
|
"disableMap": "Desactivar mapa",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Configuración para desactivar la pestaña del mapa"
|
"description": "Configuración para desactivar la pestaña del mapa"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Ocultar la pestaña del mapa para reducir el uso de batería",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Descripción de la configuración para desactivar el mapa"
|
|
||||||
},
|
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Etiqueta de la configuración de idioma"
|
"description": "Etiqueta de la configuración de idioma"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "Etiqueta de la versión de la aplicación"
|
"description": "Etiqueta de la versión de la aplicación"
|
||||||
},
|
},
|
||||||
"appName": "Nombre de la aplicación",
|
|
||||||
"@appName": {
|
|
||||||
"description": "Etiqueta del nombre de la aplicación"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Acerca de MeshCore SAR",
|
"aboutMeshCoreSar": "Acerca de MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "Título del diálogo acerca de"
|
"description": "Título del diálogo acerca de"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Acción para restablecer la ruta del contacto para re-enrutamiento"
|
"description": "Acción para restablecer la ruta del contacto para re-enrutamiento"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Clave pública copiada al portapapeles",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Mensaje de éxito cuando se copia la clave pública"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} copiado al portapapeles",
|
"copiedToClipboard": "{label} copiado al portapapeles",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Mensaje de éxito cuando se copia un valor al portapapeles",
|
"description": "Mensaje de éxito cuando se copia un valor al portapapeles",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Etiqueta de visto por última vez"
|
"description": "Etiqueta de visto por última vez"
|
||||||
},
|
},
|
||||||
"roomStatus": "Estado de la sala",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Encabezado de la sección de estado de la sala"
|
|
||||||
},
|
|
||||||
"loginStatus": "Estado de inicio de sesión",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Etiqueta de estado de inicio de sesión"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "No ha iniciado sesión",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Estado de no ha iniciado sesión"
|
|
||||||
},
|
|
||||||
"adminAccess": "Acceso de administrador",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Etiqueta de acceso de administrador"
|
|
||||||
},
|
|
||||||
"yes": "Sí",
|
"yes": "Sí",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Respuesta sí"
|
"description": "Respuesta sí"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Etiqueta de permisos"
|
"description": "Etiqueta de permisos"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Contraseña guardada",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Etiqueta de contraseña guardada"
|
|
||||||
},
|
|
||||||
"locationColon": "Ubicación:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Encabezado de la sección de ubicación"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetría",
|
"telemetry": "Telemetría",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Encabezado de la sección de telemetría"
|
"description": "Encabezado de la sección de telemetría"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "Etiqueta de GPS desde telemetría"
|
"description": "Etiqueta de GPS desde telemetría"
|
||||||
},
|
},
|
||||||
"updated": "Actualizado",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Etiqueta de marca de tiempo actualizada"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Ruta restablecida para {name}. El próximo mensaje encontrará una nueva ruta.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Mensaje de información después de restablecer ruta",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Re-iniciar sesión en la sala",
|
"reLoginToRoom": "Re-iniciar sesión en la sala",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Botón para re-iniciar sesión en la sala"
|
"description": "Botón para re-iniciar sesión en la sala"
|
||||||
@@ -1114,10 +1057,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Etiqueta del campo de frecuencia de radio"
|
"description": "Etiqueta del campo de frecuencia de radio"
|
||||||
},
|
},
|
||||||
"frequencyExample": "ej., 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Texto de ayuda de ejemplo para frecuencia"
|
|
||||||
},
|
|
||||||
"bandwidth": "Ancho de banda",
|
"bandwidth": "Ancho de banda",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Etiqueta del menú desplegable de ancho de banda"
|
"description": "Etiqueta del menú desplegable de ancho de banda"
|
||||||
@@ -1221,47 +1160,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Tooltip de controles del rastro"
|
"description": "Tooltip de controles del rastro"
|
||||||
},
|
},
|
||||||
"contactTrails": "Rastros de contactos",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Encabezado de la sección de rastros de contactos"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Mostrar todos los rastros de contactos",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Etiqueta del interruptor para mostrar todos los rastros de contactos"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "No hay contactos con historial de ubicación",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtítulo cuando no hay contactos con rastros"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Mostrando rastros para {count} contactos",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtítulo que muestra el número de contactos con rastros",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Rastros individuales de contactos",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Título del elemento expandible para rastros individuales de contactos"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Información del dispositivo",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Encabezado de la sección de información del dispositivo"
|
|
||||||
},
|
|
||||||
"bleName": "Nombre BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Etiqueta del nombre del dispositivo Bluetooth Low Energy"
|
|
||||||
},
|
|
||||||
"meshName": "Nombre Mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Etiqueta del nombre de red mesh"
|
|
||||||
},
|
|
||||||
"notSet": "No establecido",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Etiqueta cuando no se establece un valor"
|
|
||||||
},
|
|
||||||
"model": "Modelo",
|
"model": "Modelo",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Etiqueta del modelo del dispositivo"
|
"description": "Etiqueta del modelo del dispositivo"
|
||||||
@@ -1270,14 +1168,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Etiqueta de versión"
|
"description": "Etiqueta de versión"
|
||||||
},
|
},
|
||||||
"buildDate": "Fecha de compilación",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Etiqueta de fecha de compilación del firmware"
|
|
||||||
},
|
|
||||||
"firmware": "Firmware",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Etiqueta de firmware"
|
|
||||||
},
|
|
||||||
"maxContacts": "Contactos máximos",
|
"maxContacts": "Contactos máximos",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Etiqueta de capacidad máxima de contactos"
|
"description": "Etiqueta de capacidad máxima de contactos"
|
||||||
@@ -1290,26 +1180,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Encabezado de la sección de información pública"
|
"description": "Encabezado de la sección de información pública"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Nombre de red Mesh",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Etiqueta del campo de nombre de red mesh"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Nombre difundido en anuncios mesh",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Texto de ayuda para el campo de nombre de red mesh"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetría y compartir ubicación",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Etiqueta del alternar de telemetría y compartir ubicación"
|
|
||||||
},
|
|
||||||
"lat": "Lat",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Etiqueta del campo de latitud (forma corta)"
|
|
||||||
},
|
|
||||||
"lon": "Lon",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Etiqueta del campo de longitud (forma corta)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Usar ubicación actual",
|
"useCurrentLocation": "Usar ubicación actual",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip para el botón de usar ubicación actual"
|
"description": "Tooltip para el botón de usar ubicación actual"
|
||||||
@@ -1339,15 +1209,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "Copiado {label} al portapapeles",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Mensaje corto de éxito al copiar al portapapeles",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Error al guardar: {error}",
|
"failedToSave": "Error al guardar: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Mensaje de error genérico para fallos al guardar",
|
"description": "Mensaje de error genérico para fallos al guardar",
|
||||||
@@ -1367,7 +1228,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sarTemplates": "Plantillas SAR",
|
"sarTemplates": "Plantillas SAR",
|
||||||
"manageSarTemplates": "Gestionar plantillas SAR",
|
|
||||||
"addTemplate": "Agregar plantilla",
|
"addTemplate": "Agregar plantilla",
|
||||||
"editTemplate": "Editar plantilla",
|
"editTemplate": "Editar plantilla",
|
||||||
"deleteTemplate": "Eliminar plantilla",
|
"deleteTemplate": "Eliminar plantilla",
|
||||||
@@ -1395,7 +1255,6 @@
|
|||||||
"noTemplates": "No hay plantillas disponibles",
|
"noTemplates": "No hay plantillas disponibles",
|
||||||
"tapAddToCreate": "Toca + para crear tu primera plantilla",
|
"tapAddToCreate": "Toca + para crear tu primera plantilla",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permissionsSection": "Permisos",
|
|
||||||
"locationPermission": "Permiso de ubicación",
|
"locationPermission": "Permiso de ubicación",
|
||||||
"checking": "Comprobando...",
|
"checking": "Comprobando...",
|
||||||
"locationPermissionGrantedAlways": "Concedido (Siempre)",
|
"locationPermissionGrantedAlways": "Concedido (Siempre)",
|
||||||
@@ -1535,28 +1394,6 @@
|
|||||||
"wizardNext": "Siguiente",
|
"wizardNext": "Siguiente",
|
||||||
"wizardGetStarted": "Comenzar",
|
"wizardGetStarted": "Comenzar",
|
||||||
"wizardWelcomeTitle": "Bienvenido a MeshCore SAR",
|
"wizardWelcomeTitle": "Bienvenido a MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Una poderosa herramienta de comunicación sin conexión para operaciones de búsqueda y rescate. Conéctese con su equipo usando tecnología de radio en malla cuando las redes tradicionales no estén disponibles.",
|
|
||||||
"wizardConnectingTitle": "Conectando a su Radio",
|
|
||||||
"wizardConnectingDescription": "Conecte su smartphone a un dispositivo de radio MeshCore vía Bluetooth para comenzar a comunicarse sin conexión.",
|
|
||||||
"wizardConnectingFeature1": "Buscar dispositivos MeshCore cercanos",
|
|
||||||
"wizardConnectingFeature2": "Emparejar con su radio vía Bluetooth",
|
|
||||||
"wizardConnectingFeature3": "Funciona completamente sin conexión - no se requiere internet",
|
|
||||||
"wizardChannelTitle": "Canales",
|
|
||||||
"wizardChannelDescription": "Transmita mensajes a todos en un canal, perfecto para anuncios y coordinación de todo el equipo.",
|
|
||||||
"wizardChannelFeature1": "Canal público para comunicación general del equipo",
|
|
||||||
"wizardChannelFeature2": "Cree canales personalizados para grupos específicos",
|
|
||||||
"wizardChannelFeature3": "Los mensajes se retransmiten automáticamente por la malla",
|
|
||||||
"wizardContactsTitle": "Contactos",
|
|
||||||
"wizardContactsDescription": "Los miembros de su equipo aparecen automáticamente cuando se unen a la red en malla. Envíeles mensajes directos o vea su ubicación.",
|
|
||||||
"wizardContactsFeature1": "Contactos descubiertos automáticamente",
|
|
||||||
"wizardContactsFeature2": "Enviar mensajes directos privados",
|
|
||||||
"wizardContactsFeature3": "Ver nivel de batería y hora de última vista",
|
|
||||||
"wizardMapTitle": "Mapa & Ubicación",
|
|
||||||
"wizardMapDescription": "Rastree a su equipo en tiempo real y marque ubicaciones importantes para operaciones de búsqueda y rescate.",
|
|
||||||
"wizardMapFeature1": "Marcadores SAR para personas encontradas, incendios y áreas de preparación",
|
|
||||||
"wizardMapFeature2": "Rastreo GPS en tiempo real de miembros del equipo",
|
|
||||||
"wizardMapFeature3": "Descargar mapas sin conexión para áreas remotas",
|
|
||||||
"wizardMapFeature4": "Dibujar formas y compartir información táctica",
|
|
||||||
"viewWelcomeTutorial": "Ver tutorial de bienvenida",
|
"viewWelcomeTutorial": "Ver tutorial de bienvenida",
|
||||||
"allTeamContacts": "Todos los contactos del equipo",
|
"allTeamContacts": "Todos los contactos del equipo",
|
||||||
"directMessagesInfo": "Mensajes directos con confirmaciones. Enviado a {count} miembros del equipo.",
|
"directMessagesInfo": "Mensajes directos con confirmaciones. Enviado a {count} miembros del equipo.",
|
||||||
@@ -1638,5 +1475,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Activo",
|
||||||
|
"addContact": "Añadir contacto",
|
||||||
|
"all": "Todo",
|
||||||
|
"clearAllLabel": "Borrar todo",
|
||||||
|
"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",
|
||||||
|
"trace": "Traza",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"voiceLimiter": "Limitador de voz",
|
||||||
|
"micAutoGain": "Ganancia automática del micrófono",
|
||||||
|
"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",
|
||||||
|
"busy": "Ocupado",
|
||||||
|
"searchMessages": "Buscar mensajes",
|
||||||
|
"sendImageFromGallery": "Enviar imagen desde galería",
|
||||||
|
"takePhoto": "Tomar foto",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Envío automático",
|
||||||
|
"autoSendDescription": "Enviar por la ruta actual.",
|
||||||
|
"sendDirect": "Envío directo",
|
||||||
|
"sendDirectDescription": "Enviar directamente a este contacto.",
|
||||||
|
"sendFlood": "Envío por inundación",
|
||||||
|
"sendFloodDescription": "Enviar a través de todos los repetidores.",
|
||||||
|
"postConnectDiscoveryTitle": "¿Descubrir repetidores ahora?",
|
||||||
|
"postConnectDiscoveryDescription": "Ejecutar descubrimiento de repetidores justo después de conectarse para ver nodos MeshCore cercanos y agregarlos a su red más rápido.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Agregar",
|
||||||
|
"addAdditionalDetails": "Agregar detalles adicionales",
|
||||||
|
"addPeer": "Agregar par",
|
||||||
|
"addPeerManually": "Agregar par manualmente",
|
||||||
|
"addedToContacts": "{name} agregado a contactos",
|
||||||
|
"addedToSensors": "{name} agregado a Sensores",
|
||||||
|
"allowAll": "Permitir todos",
|
||||||
|
"anonymousRxStats": "Estadísticas RX anónimas",
|
||||||
|
"appearance": "Apariencia",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Telemetría base",
|
||||||
|
"blePin": "PIN BLE",
|
||||||
|
"buzzerAlerts": "Alertas de zumbador",
|
||||||
|
"cancelImageReceive": "Cancelar recepción de imagen",
|
||||||
|
"choose": "Elegir",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Elige qué pestañas y secciones de contactos mostrar",
|
||||||
|
"clearCache": "Limpiar caché",
|
||||||
|
"clearHistory": "Limpiar historial",
|
||||||
|
"clearLiveView": "Limpiar vista en vivo",
|
||||||
|
"clearLogs": "Limpiar registros",
|
||||||
|
"clearOfflineCache": "¿Limpiar caché sin conexión?",
|
||||||
|
"clearOverlay": "Limpiar superposición",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Limpiar ruta solo después de que fallen todos los reintentos",
|
||||||
|
"codecBitrateAndAudioProcessing": "Códec, tasa de bits y procesamiento de audio",
|
||||||
|
"connectByIpAddress": "Conectar por dirección IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Controla qué alertas recibes",
|
||||||
|
"copyFullHex": "Copiar Hex completo",
|
||||||
|
"copyRawResponse": "Copiar respuesta sin procesar",
|
||||||
|
"couldNotLoadMetHistory": "No se pudo cargar el historial MET",
|
||||||
|
"customizeSensor": "Personalizar {name}",
|
||||||
|
"data": "Datos",
|
||||||
|
"deleteGroup": "Eliminar grupo",
|
||||||
|
"deleteStyleConfirm": "¿Eliminar {name}?",
|
||||||
|
"deleteThisDrawing": "¿Eliminar este {type}?",
|
||||||
|
"deny": "Denegar",
|
||||||
|
"deviceInfo": "Información del dispositivo",
|
||||||
|
"deviceSettings": "Configuración del dispositivo",
|
||||||
|
"deviceTimeSynced": "Hora del dispositivo sincronizada con este teléfono.",
|
||||||
|
"disableFastGpsPublishing": "Desactivar publicación rápida de GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Pantalla, marcadores y base de datos de trazas",
|
||||||
|
"download": "Descargar",
|
||||||
|
"environmentalTelemetry": "Telemetría ambiental",
|
||||||
|
"errorCheckingForUpdates": "Error al buscar actualizaciones: {error}",
|
||||||
|
"errorGeneric": "Error: {error}",
|
||||||
|
"errorOpeningDownload": "Error al abrir la descarga: {error}",
|
||||||
|
"exportLogs": "Exportar registros",
|
||||||
|
"extraAggressiveAvifCompression": "Compresión AVIF extra agresiva",
|
||||||
|
"failedToAddContact": "Error al agregar contacto: {error}",
|
||||||
|
"failedToAddName": "Error al agregar {name}: {error}",
|
||||||
|
"failedToClearChannels": "Error al limpiar canales: {error}",
|
||||||
|
"failedToClearContacts": "Error al limpiar contactos: {error}",
|
||||||
|
"failedToConnectViaSerial": "Error al conectar por serial",
|
||||||
|
"failedToLoadPreviewImage": "Error al cargar imagen de vista previa: {error}",
|
||||||
|
"failedToLoadTrace": "Error al cargar traza: {error}",
|
||||||
|
"failedToSendSarMarker": "Error al enviar marcador SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Error al configurar modo de zumbador: {error}",
|
||||||
|
"failedToSetGpsMode": "Error al configurar modo GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Error al sincronizar hora del dispositivo: {error}",
|
||||||
|
"failedToWipeDeviceData": "Error al borrar datos del dispositivo: {error}",
|
||||||
|
"fastGpsTargetChannel": "Canal objetivo de GPS rápido",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Actualizaciones rápidas, umbrales y permisos",
|
||||||
|
"fetchingCatalog": "Obteniendo catálogo...",
|
||||||
|
"fifteenMinutes": "15 minutos",
|
||||||
|
"fifteenSeconds": "15 segundos",
|
||||||
|
"filterByDirection": "Filtrar por dirección",
|
||||||
|
"finish": "Finalizar",
|
||||||
|
"fiveMinutes": "5 minutos",
|
||||||
|
"fiveSeconds": "5 segundos",
|
||||||
|
"fix": "Posición",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Política de anuncio GPS",
|
||||||
|
"gpsAndLocation": "GPS y ubicación",
|
||||||
|
"gpsDiagnostics": "Diagnósticos GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Ayuda",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Oculto",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Cómo la radio agrega automáticamente nodos descubiertos.",
|
||||||
|
"images": "Imágenes",
|
||||||
|
"importProfile": "Importar perfil",
|
||||||
|
"lastFix": "Última posición",
|
||||||
|
"latest": "Más reciente",
|
||||||
|
"latitude": "Latitud",
|
||||||
|
"loadASavedRegion": "Cargar una región guardada",
|
||||||
|
"loading": "Cargando",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Compartir ubicación, hardware e intervalo de actualización.",
|
||||||
|
"locationTelemetry": "Telemetría de ubicación",
|
||||||
|
"lockDestination": "Bloquear destino",
|
||||||
|
"longitude": "Longitud",
|
||||||
|
"mapStyle": "Estilo de mapa",
|
||||||
|
"max": "Máx",
|
||||||
|
"maxZoom": "Zoom máximo",
|
||||||
|
"messaging": "Mensajería",
|
||||||
|
"metHistory": "Historial MET",
|
||||||
|
"min": "Mín",
|
||||||
|
"minZoom": "Zoom mínimo",
|
||||||
|
"moveDown": "Mover abajo",
|
||||||
|
"moveUp": "Mover arriba",
|
||||||
|
"multiAckMode": "Modo multi-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Gestión de espacio de trabajo multidispositivo",
|
||||||
|
"myLocation": "Mi ubicación",
|
||||||
|
"nameAndTelemetryShared": "Nombre y telemetría compartidos con otros dispositivos.",
|
||||||
|
"newVersionAvailable": "Nueva versión: {version}",
|
||||||
|
"newer": "Más nuevo",
|
||||||
|
"noNeighboursFound": "No se encontraron vecinos",
|
||||||
|
"noServersFound": "No se encontraron servidores",
|
||||||
|
"notSet": "Sin configurar",
|
||||||
|
"notifications": "Notificaciones",
|
||||||
|
"offZeroSeconds": "Apagado (0 s)",
|
||||||
|
"offlineMaps": "Mapas sin conexión",
|
||||||
|
"older": "Más antiguo",
|
||||||
|
"oneByteMode0": "1 byte (modo 0)",
|
||||||
|
"oneHour": "1 hora",
|
||||||
|
"oneMinute": "1 minuto",
|
||||||
|
"openPacketLogs": "Abrir registros de paquetes",
|
||||||
|
"packetTypeHelp": "Ayuda de tipo de paquete",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Pega un enlace compartido o anuncio hex",
|
||||||
|
"pathHash": "Hash de ruta",
|
||||||
|
"pathSize": "Tamaño de ruta",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Polígono",
|
||||||
|
"rawResponseCopied": "Respuesta sin procesar copiada",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Reenviar a través del repetidor más cercano en caso de fallo",
|
||||||
|
"resolveContact": "Resolver contacto",
|
||||||
|
"routingRetriesAndDestinationLock": "Enrutamiento, reintentos y bloqueo de destino",
|
||||||
|
"samples": "Muestras",
|
||||||
|
"satellites": "Satélites",
|
||||||
|
"saving": "Guardando...",
|
||||||
|
"search": "Buscar",
|
||||||
|
"searchByNameOrToken": "Buscar por nombre o token",
|
||||||
|
"searchDiscoveredNodes": "Buscar nodos descubiertos",
|
||||||
|
"searchInCurrentFilter": "Buscar en el filtro actual",
|
||||||
|
"searchLogs": "Buscar registros...",
|
||||||
|
"searchSensors": "Buscar sensores",
|
||||||
|
"searchThisSection": "Buscar en esta sección",
|
||||||
|
"selectedRelays": "Relés seleccionados",
|
||||||
|
"sensor": "Sensor",
|
||||||
|
"serialError": "Error serial: {error}",
|
||||||
|
"setPathForContact": "Establecer ruta para {name}",
|
||||||
|
"shareLiveGps": "Compartir GPS en vivo",
|
||||||
|
"shareMyTiles": "Compartir mis mosaicos",
|
||||||
|
"sharingLocationFromPhone": "Compartiendo ubicación en este canal desde el teléfono.",
|
||||||
|
"sharingLocationFromRadio": "Compartiendo ubicación en este canal desde la radio.",
|
||||||
|
"signalDbm": "Señal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Tamaño, compresión y vista previa",
|
||||||
|
"skip": "Omitir",
|
||||||
|
"smallerFileSize": "Tamaño de archivo más pequeño",
|
||||||
|
"sort": "Ordenar",
|
||||||
|
"stoppedSharingLocation": "Se dejó de compartir ubicación en este canal.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Suprimir notificaciones en primer plano",
|
||||||
|
"tabsAndNavigation": "Pestañas y navegación",
|
||||||
|
"templatesAndTutorials": "Plantillas y tutoriales",
|
||||||
|
"tenMinutes": "10 minutos",
|
||||||
|
"tenSeconds": "10 segundos",
|
||||||
|
"testSendUpdate": "Probar envío de actualización",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, idioma y preferencias de visualización",
|
||||||
|
"thirtyMinutes": "30 minutos",
|
||||||
|
"thirtySeconds": "30 segundos",
|
||||||
|
"threeBytesMode2": "3 bytes (modo 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Estadísticas de tráfico, historial de mensajes y herramientas de desarrollo",
|
||||||
|
"twoBytesMode1": "2 bytes (modo 1)",
|
||||||
|
"twoMinutes": "2 minutos",
|
||||||
|
"undo": "Deshacer",
|
||||||
|
"updated": "Actualizado",
|
||||||
|
"useContactFlags": "Usar banderas de contacto",
|
||||||
|
"useSavedCoordinates": "Usar coordenadas guardadas",
|
||||||
|
"view": "Ver",
|
||||||
|
"viewNeighbours": "Ver vecinos",
|
||||||
|
"viewPublicStats": "Ver estadísticas públicas",
|
||||||
|
"setRegionScope": "Establecer ámbito de región",
|
||||||
|
"regionScope": "Ámbito de región",
|
||||||
|
"regionScopeNone": "Ninguno (global)",
|
||||||
|
"clearRegionScope": "Limpiar ámbito",
|
||||||
|
"regionScopeWarning": "Solo los repetidores que permitan esta región reenviarán.",
|
||||||
|
"discoverRegions": "Descubrir desde repetidores",
|
||||||
|
"discoveringRegions": "Descubriendo regiones...",
|
||||||
|
"enterRegionName": "Nombre de región (ej. auckland)",
|
||||||
|
"noRegionsFound": "No se encontraron regiones en este repetidor.",
|
||||||
|
"regionScopeSet": "Ámbito de región establecido a {name}",
|
||||||
|
"regionScopeCleared": "Ámbito de región limpiado",
|
||||||
|
"simpleMode": "Modo simple",
|
||||||
|
"simpleModeDescription": "Solo chat y mapa con contactos favoritos",
|
||||||
|
"simpleModeRequiresLockedDestination": "Activa primero «Bloquear destino» con un canal para usar el modo simple",
|
||||||
|
"noFavouriteContacts": "Sin contactos favoritos",
|
||||||
|
"selectDestinationFirst": "Selecciona primero un canal, contacto o sala",
|
||||||
|
"recipientNoLongerAvailable": "El destinatario ya no está disponible",
|
||||||
|
"failedToSendMessage": "Error al enviar: {error}",
|
||||||
|
"tictactoeDirectOnly": "El tres en raya solo funciona en mensajes directos. Elige primero un contacto.",
|
||||||
|
"deviceKeyUnavailable": "La clave del dispositivo no está disponible",
|
||||||
|
"imageCompressionFailed": "Error al comprimir la imagen",
|
||||||
|
"imageFragmentationFailed": "Error al fragmentar la imagen",
|
||||||
|
"failedToAnnounceImage": "Error al anunciar la imagen",
|
||||||
|
"noRecipientSelected": "Ningún destinatario seleccionado",
|
||||||
|
"imageSendFailed": "Error al enviar la imagen",
|
||||||
|
"microphonePermissionRequiredForVoice": "Se necesita permiso de micrófono para los mensajes de voz",
|
||||||
|
"recordingDiscarded": "Grabación descartada",
|
||||||
|
"publicChannelMediaWarning": "Estás a punto de enviar {mediaType} al canal público. No es recomendable, porque cualquiera en la red mesh podría recibirlo. Elige un canal privado o etiquetado, salvo que sea lo que quieres.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Selecciona un canal privado para compartir tu ubicación.",
|
||||||
|
"stopSharingMyLocation": "Dejar de compartir mi ubicación",
|
||||||
|
"shareMyLocation": "Compartir mi ubicación",
|
||||||
|
"stopRecording": "Detener grabación",
|
||||||
|
"recordVoice": "Grabar voz",
|
||||||
|
"moreActions": "Más acciones",
|
||||||
|
"noMessagesInFilter": "No hay mensajes en este filtro",
|
||||||
|
"noMatchesInFilter": "No hay coincidencias en este filtro",
|
||||||
|
"syncFailed": "Error de sincronización: {error}",
|
||||||
|
"invalidLink": "Enlace no válido",
|
||||||
|
"cannotOpenLink": "No se puede abrir el enlace",
|
||||||
|
"failedToOpenLink": "Error al abrir el enlace",
|
||||||
|
"failedToResendMessage": "Error al reenviar el mensaje",
|
||||||
|
"retryFailed": "Error al reintentar: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Eliminar dibujo y mensaje",
|
||||||
|
"lastEchoRelay": "Último eco: repetidor",
|
||||||
|
"lastEchoPath": "Último eco: ruta",
|
||||||
|
"lastEchoBytesReport": "Último eco: informe de bytes",
|
||||||
|
"noGpsCoordinatesAvailable": "No hay coordenadas GPS disponibles",
|
||||||
|
"notASarMarker": "No es un marcador SAR",
|
||||||
|
"failedToSaveTemplate": "Error al guardar la plantilla: {error}",
|
||||||
|
"drawingNotFound": "Dibujo no encontrado",
|
||||||
|
"unknownDrawingType": "Tipo de dibujo desconocido",
|
||||||
|
"customMapMarker": "Marcador de mapa personalizado",
|
||||||
|
"customMapPointLabel": "Punto: {point}",
|
||||||
|
"mapIdLabel": "ID del mapa: {id}",
|
||||||
|
"cannotFetchVoice": "No se puede obtener la voz",
|
||||||
|
"senderContactUnknown": "El contacto del remitente es desconocido. Sincroniza primero los contactos.",
|
||||||
|
"senderRouteUnknown": "La ruta del remitente es desconocida. Sincroniza primero los contactos/la ruta.",
|
||||||
|
"messageTooFar": "El mensaje está demasiado lejos ({hops} saltos, máx. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "La ruta del remitente no respondió a la comprobación de ruta. Sincroniza los contactos/la ruta e inténtalo de nuevo.",
|
||||||
|
"senderRouteNoRawResponse": "La ruta del remitente no respondió en la ruta de transporte raw.",
|
||||||
|
"voiceFetchOverHops": "Obtener la voz a través de {hops} saltos puede tardar un poco.",
|
||||||
|
"voiceReceiveCanceled": "Recepción de voz cancelada",
|
||||||
|
"fetchingMissingVoiceFragments": "Obteniendo fragmentos de voz que faltan",
|
||||||
|
"receivingVoice": "Recibiendo voz",
|
||||||
|
"cannotFetchImage": "No se puede obtener la imagen",
|
||||||
|
"imageFetchOverHops": "Obtener la imagen a través de {hops} saltos puede tardar un poco.",
|
||||||
|
"imageAlreadyBeingReceived": "La imagen ya se está recibiendo",
|
||||||
|
"loadImage": "Cargar imagen",
|
||||||
|
"imageFetchFailedToSendRequest": "No se pudo enviar la solicitud de imagen",
|
||||||
|
"imageUnavailable": "Imagen no disponible por ahora",
|
||||||
|
"imageFetchTimedOut": "La obtención de la imagen agotó el tiempo de espera",
|
||||||
|
"imageReceiveCanceled": "Recepción de imagen cancelada",
|
||||||
|
"closeImagePreview": "Cerrar vista previa de imagen",
|
||||||
|
"recordingReleaseToSend": "Grabando... suelta para enviar la voz",
|
||||||
|
"sendingVoice": "Enviando voz...",
|
||||||
|
"sendLongPressToRecordVoice": "Enviar (mantén pulsado para grabar voz)",
|
||||||
|
"discardRecording": "Descartar grabación",
|
||||||
|
"hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
|
||||||
|
"channelsSectionDescription": "Canales de difusión para el tráfico mesh cercano",
|
||||||
|
"roomsSectionDescription": "Espacios compartidos para la coordinación del equipo",
|
||||||
|
"contactsSectionDescription": "Personas y dispositivos a tu alcance directo",
|
||||||
|
"noRecentChatters": "Sin participantes recientes",
|
||||||
|
"public": "Público",
|
||||||
|
"failedToSendTicTacToeMove": "No se pudo enviar el movimiento de tres en raya",
|
||||||
|
"youWon": "Has ganado",
|
||||||
|
"opponentWon": "Ha ganado el rival",
|
||||||
|
"gameDraw": "Empate",
|
||||||
|
"yourTurn": "Te toca",
|
||||||
|
"opponentTurn": "Le toca al rival",
|
||||||
|
"pleaseEnterValidCoordinates": "Introduce coordenadas válidas",
|
||||||
|
"locationNotAvailable": "Ubicación no disponible",
|
||||||
|
"customCaveMapPoint": "Punto de mapa de cueva personalizado",
|
||||||
|
"mapPoint": "Punto del mapa",
|
||||||
|
"fetchingMissingImageFragments": "Obteniendo fragmentos que faltan…",
|
||||||
|
"loadingImage": "Cargando…",
|
||||||
|
"receivingImage": "Recibiendo…",
|
||||||
|
"tapToLoad": "Toca para cargar",
|
||||||
|
"connectDevice": "Conectar dispositivo",
|
||||||
|
"chooseTransportSubtitle": "Elige el transporte Bluetooth, WiFi o Serie",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Red",
|
||||||
|
"serial": "Serie",
|
||||||
|
"scan": "Buscar",
|
||||||
|
"pressScanToSearchForDevices": "Pulsa Buscar para encontrar dispositivos cercanos",
|
||||||
|
"failedToConnectToDevice": "Error al conectar con {name}",
|
||||||
|
"unknownDevice": "Dispositivo desconocido",
|
||||||
|
"showingCachedResultsTapRefresh": "Mostrando resultados en caché. Toca actualizar para volver a buscar.",
|
||||||
|
"scanningLocalNetworkOnPort": "Buscando dispositivos MeshCore WiFi en la red local en el puerto {port}",
|
||||||
|
"cancelScanAndAddServer": "Cancelar búsqueda y añadir servidor",
|
||||||
|
"addServer": "Añadir servidor",
|
||||||
|
"scanningProgressIps": "Buscando... {scanned}/{total} IP",
|
||||||
|
"cancelAndEnterManually": "Cancelar e introducir manualmente",
|
||||||
|
"noRecentOrDiscoveredServers": "Aún no hay servidores recientes ni descubiertos",
|
||||||
|
"recentlyUsed": "Usados recientemente",
|
||||||
|
"connecting": "Conectando...",
|
||||||
|
"discoveredOnThisNetwork": "Descubiertos en esta red",
|
||||||
|
"serverNoLongerAvailable": "El servidor en {host}:{port} ya no está disponible. Vuelve a buscar para encontrar servidores activos.",
|
||||||
|
"failedToConnectToHost": "Error al conectar con {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Introduce una dirección IP válida",
|
||||||
|
"enterValidTcpPort": "Introduce un puerto TCP válido",
|
||||||
|
"ipAddress": "Dirección IP",
|
||||||
|
"tcpPort": "Puerto TCP",
|
||||||
|
"customServerPort": "Puerto de servidor personalizado",
|
||||||
|
"chat": "Chat",
|
||||||
|
"restoringPreviousLink": "Restaurando el enlace anterior",
|
||||||
|
"noDeviceConnected": "Ningún dispositivo conectado",
|
||||||
|
"sendAdvert": "Enviar mi contacto",
|
||||||
|
"advertMode": "Enviar mi contacto",
|
||||||
|
"advertModeSubtitle": "Elige hasta dónde debe llegar tu información de contacto.",
|
||||||
|
"floodAdvertSent": "Contacto enviado a través de la red mesh",
|
||||||
|
"directAdvertSent": "Contacto enviado a dispositivos cercanos"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Bouton pour relancer la recherche BLE"
|
"description": "Bouton pour relancer la recherche BLE"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Appuyez pour connecter",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Texte de sous-titre pour l'appareil dans la liste de recherche"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Appareil non connecté",
|
"deviceNotConnected": "Appareil non connecté",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Message d'erreur lorsque l'appareil n'est pas connecté"
|
"description": "Message d'erreur lorsque l'appareil n'est pas connecté"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Paramètre pour afficher les indicateurs RX/TX"
|
"description": "Paramètre pour afficher les indicateurs RX/TX"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Afficher les indicateurs d'activité des paquets dans la barre supérieure",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description du paramètre des indicateurs RX/TX"
|
|
||||||
},
|
|
||||||
"disableMap": "Désactiver la carte",
|
"disableMap": "Désactiver la carte",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Paramètre pour désactiver l'onglet carte"
|
"description": "Paramètre pour désactiver l'onglet carte"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Masquer l'onglet carte pour réduire la consommation de batterie",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description du paramètre pour désactiver la carte"
|
|
||||||
},
|
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Libellé du paramètre de langue"
|
"description": "Libellé du paramètre de langue"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "Libellé de la version de l'application"
|
"description": "Libellé de la version de l'application"
|
||||||
},
|
},
|
||||||
"appName": "Nom de l'application",
|
|
||||||
"@appName": {
|
|
||||||
"description": "Libellé du nom de l'application"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "À propos de MeshCore SAR",
|
"aboutMeshCoreSar": "À propos de MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "Titre de la boîte de dialogue À propos"
|
"description": "Titre de la boîte de dialogue À propos"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action pour réinitialiser le chemin du contact pour réacheminement"
|
"description": "Action pour réinitialiser le chemin du contact pour réacheminement"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Clé publique copiée dans le presse-papiers",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Message de succès lorsque la clé publique est copiée"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} copié dans le presse-papiers",
|
"copiedToClipboard": "{label} copié dans le presse-papiers",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Message de succès lorsqu'une valeur est copiée dans le presse-papiers",
|
"description": "Message de succès lorsqu'une valeur est copiée dans le presse-papiers",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Libellé de la dernière vue"
|
"description": "Libellé de la dernière vue"
|
||||||
},
|
},
|
||||||
"roomStatus": "État du salon",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "En-tête de section de l'état du salon"
|
|
||||||
},
|
|
||||||
"loginStatus": "État de connexion",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Libellé de l'état de connexion"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Non connecté",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "État non connecté"
|
|
||||||
},
|
|
||||||
"adminAccess": "Accès administrateur",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Libellé de l'accès administrateur"
|
|
||||||
},
|
|
||||||
"yes": "Oui",
|
"yes": "Oui",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Réponse Oui"
|
"description": "Réponse Oui"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Libellé des permissions"
|
"description": "Libellé des permissions"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Mot de passe enregistré",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Libellé du mot de passe enregistré"
|
|
||||||
},
|
|
||||||
"locationColon": "Position :",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "En-tête de section de position"
|
|
||||||
},
|
|
||||||
"telemetry": "Télémétrie",
|
"telemetry": "Télémétrie",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "En-tête de section de télémétrie"
|
"description": "En-tête de section de télémétrie"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "Libellé du GPS depuis la télémétrie"
|
"description": "Libellé du GPS depuis la télémétrie"
|
||||||
},
|
},
|
||||||
"updated": "Mis à jour",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Libellé de l'horodatage de mise à jour"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Chemin réinitialisé pour {name}. Le prochain message trouvera un nouvel itinéraire.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Message d'information après la réinitialisation du chemin",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Se reconnecter au salon",
|
"reLoginToRoom": "Se reconnecter au salon",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Bouton pour se reconnecter au salon"
|
"description": "Bouton pour se reconnecter au salon"
|
||||||
@@ -1114,10 +1057,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Libellé du champ de fréquence radio"
|
"description": "Libellé du champ de fréquence radio"
|
||||||
},
|
},
|
||||||
"frequencyExample": "ex. : 869,618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Texte d'aide exemple pour la fréquence"
|
|
||||||
},
|
|
||||||
"bandwidth": "Bande passante",
|
"bandwidth": "Bande passante",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Libellé du menu déroulant de bande passante"
|
"description": "Libellé du menu déroulant de bande passante"
|
||||||
@@ -1221,47 +1160,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Info-bulle des contrôles de la trace"
|
"description": "Info-bulle des contrôles de la trace"
|
||||||
},
|
},
|
||||||
"contactTrails": "Traces des contacts",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "En-tête de section des traces des contacts"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Afficher toutes les traces des contacts",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Étiquette du commutateur pour afficher toutes les traces des contacts"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Aucun contact avec historique de localisation",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Sous-titre lorsqu'il n'y a pas de contacts avec des traces"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Affichage des traces pour {count} contacts",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Sous-titre montrant le nombre de contacts avec des traces",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Traces individuelles des contacts",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Titre de l'élément extensible pour les traces individuelles des contacts"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Informations sur l'appareil",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "En-tête de section des informations sur l'appareil"
|
|
||||||
},
|
|
||||||
"bleName": "Nom BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Libellé du nom de l'appareil Bluetooth Low Energy"
|
|
||||||
},
|
|
||||||
"meshName": "Nom du maillage",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Libellé du nom du réseau maillé"
|
|
||||||
},
|
|
||||||
"notSet": "Non défini",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Libellé lorsqu'une valeur n'est pas définie"
|
|
||||||
},
|
|
||||||
"model": "Modèle",
|
"model": "Modèle",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Libellé du modèle d'appareil"
|
"description": "Libellé du modèle d'appareil"
|
||||||
@@ -1270,14 +1168,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Libellé de la version"
|
"description": "Libellé de la version"
|
||||||
},
|
},
|
||||||
"buildDate": "Date de compilation",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Libellé de la date de compilation du micrologiciel"
|
|
||||||
},
|
|
||||||
"firmware": "Micrologiciel",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Libellé du micrologiciel"
|
|
||||||
},
|
|
||||||
"maxContacts": "Contacts max",
|
"maxContacts": "Contacts max",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Libellé de la capacité maximale de contacts"
|
"description": "Libellé de la capacité maximale de contacts"
|
||||||
@@ -1290,26 +1180,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "En-tête de section des informations publiques"
|
"description": "En-tête de section des informations publiques"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Nom du réseau maillé",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Libellé du champ du nom du réseau maillé"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Nom diffusé dans les annonces du maillage",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Texte d'aide pour le champ du nom du réseau maillé"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Télémétrie et partage de position",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Libellé de l'interrupteur de télémétrie et de partage de position"
|
|
||||||
},
|
|
||||||
"lat": "Lat",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Libellé du champ de latitude (forme courte)"
|
|
||||||
},
|
|
||||||
"lon": "Lon",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Libellé du champ de longitude (forme courte)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Utiliser la position actuelle",
|
"useCurrentLocation": "Utiliser la position actuelle",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Info-bulle du bouton d'utilisation de la position actuelle"
|
"description": "Info-bulle du bouton d'utilisation de la position actuelle"
|
||||||
@@ -1339,15 +1209,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} copié dans le presse-papiers",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Message de succès court lors de la copie dans le presse-papiers",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Échec de l'enregistrement : {error}",
|
"failedToSave": "Échec de l'enregistrement : {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Message d'erreur générique pour les échecs d'enregistrement",
|
"description": "Message d'erreur générique pour les échecs d'enregistrement",
|
||||||
@@ -1367,7 +1228,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sarTemplates": "Modèles SAR",
|
"sarTemplates": "Modèles SAR",
|
||||||
"manageSarTemplates": "Gérer les modèles SAR",
|
|
||||||
"addTemplate": "Ajouter un modèle",
|
"addTemplate": "Ajouter un modèle",
|
||||||
"editTemplate": "Modifier le modèle",
|
"editTemplate": "Modifier le modèle",
|
||||||
"deleteTemplate": "Supprimer le modèle",
|
"deleteTemplate": "Supprimer le modèle",
|
||||||
@@ -1395,7 +1255,6 @@
|
|||||||
"noTemplates": "Aucun modele disponible",
|
"noTemplates": "Aucun modele disponible",
|
||||||
"tapAddToCreate": "Appuyez sur + pour creer votre premier modele",
|
"tapAddToCreate": "Appuyez sur + pour creer votre premier modele",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permissionsSection": "Autorisations",
|
|
||||||
"locationPermission": "Autorisation de localisation",
|
"locationPermission": "Autorisation de localisation",
|
||||||
"checking": "Vérification...",
|
"checking": "Vérification...",
|
||||||
"locationPermissionGrantedAlways": "Accordée (Toujours)",
|
"locationPermissionGrantedAlways": "Accordée (Toujours)",
|
||||||
@@ -1538,28 +1397,6 @@
|
|||||||
"wizardNext": "Suivant",
|
"wizardNext": "Suivant",
|
||||||
"wizardGetStarted": "Commencer",
|
"wizardGetStarted": "Commencer",
|
||||||
"wizardWelcomeTitle": "Bienvenue dans MeshCore SAR",
|
"wizardWelcomeTitle": "Bienvenue dans MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Un outil de communication hors ligne puissant pour les opérations de recherche et de sauvetage. Connectez-vous avec votre équipe en utilisant la technologie radio maillée lorsque les réseaux traditionnels ne sont pas disponibles.",
|
|
||||||
"wizardConnectingTitle": "Connexion à votre Radio",
|
|
||||||
"wizardConnectingDescription": "Connectez votre smartphone à un appareil radio MeshCore via Bluetooth pour commencer à communiquer hors ligne.",
|
|
||||||
"wizardConnectingFeature1": "Rechercher les appareils MeshCore à proximité",
|
|
||||||
"wizardConnectingFeature2": "Coupler avec votre radio via Bluetooth",
|
|
||||||
"wizardConnectingFeature3": "Fonctionne entièrement hors ligne - aucun internet requis",
|
|
||||||
"wizardChannelTitle": "Canaux",
|
|
||||||
"wizardChannelDescription": "Diffusez des messages à tous sur un canal, parfait pour les annonces et la coordination de toute l'équipe.",
|
|
||||||
"wizardChannelFeature1": "Canal public pour la communication générale de l'équipe",
|
|
||||||
"wizardChannelFeature2": "Créer des canaux personnalisés pour des groupes spécifiques",
|
|
||||||
"wizardChannelFeature3": "Les messages sont automatiquement relayés par le maillage",
|
|
||||||
"wizardContactsTitle": "Contacts",
|
|
||||||
"wizardContactsDescription": "Les membres de votre équipe apparaissent automatiquement lorsqu'ils rejoignent le réseau maillé. Envoyez-leur des messages directs ou consultez leur emplacement.",
|
|
||||||
"wizardContactsFeature1": "Contacts découverts automatiquement",
|
|
||||||
"wizardContactsFeature2": "Envoyer des messages directs privés",
|
|
||||||
"wizardContactsFeature3": "Voir le niveau de batterie et l'heure de dernière vue",
|
|
||||||
"wizardMapTitle": "Carte & Localisation",
|
|
||||||
"wizardMapDescription": "Suivez votre équipe en temps réel et marquez les emplacements importants pour les opérations de recherche et de sauvetage.",
|
|
||||||
"wizardMapFeature1": "Marqueurs SAR pour les personnes retrouvées, les incendies et les zones de rassemblement",
|
|
||||||
"wizardMapFeature2": "Suivi GPS en temps réel des membres de l'équipe",
|
|
||||||
"wizardMapFeature3": "Télécharger des cartes hors ligne pour les zones éloignées",
|
|
||||||
"wizardMapFeature4": "Dessiner des formes et partager des informations tactiques",
|
|
||||||
"viewWelcomeTutorial": "Voir le tutoriel de bienvenue",
|
"viewWelcomeTutorial": "Voir le tutoriel de bienvenue",
|
||||||
"allTeamContacts": "Tous les contacts de l'équipe",
|
"allTeamContacts": "Tous les contacts de l'équipe",
|
||||||
"directMessagesInfo": "Messages directs avec confirmations. Envoyé à {count} membres de l'équipe.",
|
"directMessagesInfo": "Messages directs avec confirmations. Envoyé à {count} membres de l'équipe.",
|
||||||
@@ -1641,5 +1478,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Actif",
|
||||||
|
"addContact": "Ajouter un contact",
|
||||||
|
"all": "Tout",
|
||||||
|
"clearAllLabel": "Tout effacer",
|
||||||
|
"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",
|
||||||
|
"trace": "Trace",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"voiceLimiter": "Limiteur vocal",
|
||||||
|
"micAutoGain": "Gain auto du micro",
|
||||||
|
"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",
|
||||||
|
"busy": "Occupé",
|
||||||
|
"searchMessages": "Rechercher des messages",
|
||||||
|
"sendImageFromGallery": "Envoyer une image de la galerie",
|
||||||
|
"takePhoto": "Prendre une photo",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Envoi automatique",
|
||||||
|
"autoSendDescription": "Envoyer via le chemin actuel.",
|
||||||
|
"sendDirect": "Envoi direct",
|
||||||
|
"sendDirectDescription": "Envoyer directement à ce contact.",
|
||||||
|
"sendFlood": "Envoi par inondation",
|
||||||
|
"sendFloodDescription": "Envoyer via tous les relais.",
|
||||||
|
"postConnectDiscoveryTitle": "Découvrir les relais maintenant ?",
|
||||||
|
"postConnectDiscoveryDescription": "Lancer la découverte des relais juste après la connexion pour voir les nœuds MeshCore à proximité et les ajouter plus rapidement à votre réseau.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Ajouter",
|
||||||
|
"addAdditionalDetails": "Ajouter des détails supplémentaires",
|
||||||
|
"addPeer": "Ajouter un pair",
|
||||||
|
"addPeerManually": "Ajouter un pair manuellement",
|
||||||
|
"addedToContacts": "{name} ajouté aux contacts",
|
||||||
|
"addedToSensors": "{name} ajouté aux capteurs",
|
||||||
|
"allowAll": "Tout autoriser",
|
||||||
|
"anonymousRxStats": "Statistiques RX anonymes",
|
||||||
|
"appearance": "Apparence",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Télémétrie de base",
|
||||||
|
"blePin": "PIN BLE",
|
||||||
|
"buzzerAlerts": "Alertes sonores",
|
||||||
|
"cancelImageReceive": "Annuler la réception d'image",
|
||||||
|
"choose": "Choisir",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Choisir les onglets et sections de contacts à afficher",
|
||||||
|
"clearCache": "Vider le cache",
|
||||||
|
"clearHistory": "Effacer l'historique",
|
||||||
|
"clearLiveView": "Effacer la vue en direct",
|
||||||
|
"clearLogs": "Effacer les journaux",
|
||||||
|
"clearOfflineCache": "Vider le cache hors ligne ?",
|
||||||
|
"clearOverlay": "Effacer la superposition",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Effacer la route uniquement après l'échec de toutes les tentatives",
|
||||||
|
"codecBitrateAndAudioProcessing": "Codec, débit et traitement audio",
|
||||||
|
"connectByIpAddress": "Connexion par adresse IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Contrôler les alertes que vous recevez",
|
||||||
|
"copyFullHex": "Copier le Hex complet",
|
||||||
|
"copyRawResponse": "Copier la réponse brute",
|
||||||
|
"couldNotLoadMetHistory": "Impossible de charger l'historique MET",
|
||||||
|
"customizeSensor": "Personnaliser {name}",
|
||||||
|
"data": "Données",
|
||||||
|
"deleteGroup": "Supprimer le groupe",
|
||||||
|
"deleteStyleConfirm": "Supprimer {name} ?",
|
||||||
|
"deleteThisDrawing": "Supprimer ce {type} ?",
|
||||||
|
"deny": "Refuser",
|
||||||
|
"deviceInfo": "Infos de l'appareil",
|
||||||
|
"deviceSettings": "Paramètres de l'appareil",
|
||||||
|
"deviceTimeSynced": "Heure de l'appareil synchronisée avec ce téléphone.",
|
||||||
|
"disableFastGpsPublishing": "Désactiver la publication GPS rapide",
|
||||||
|
"displayMarkersAndTraceDatabase": "Affichage, marqueurs et base de données de traces",
|
||||||
|
"download": "Télécharger",
|
||||||
|
"environmentalTelemetry": "Télémétrie environnementale",
|
||||||
|
"errorCheckingForUpdates": "Erreur lors de la vérification des mises à jour : {error}",
|
||||||
|
"errorGeneric": "Erreur : {error}",
|
||||||
|
"errorOpeningDownload": "Erreur lors de l'ouverture du téléchargement : {error}",
|
||||||
|
"exportLogs": "Exporter les journaux",
|
||||||
|
"extraAggressiveAvifCompression": "Compression AVIF extra-agressive",
|
||||||
|
"failedToAddContact": "Échec de l'ajout du contact : {error}",
|
||||||
|
"failedToAddName": "Échec de l'ajout de {name} : {error}",
|
||||||
|
"failedToClearChannels": "Échec de l'effacement des canaux : {error}",
|
||||||
|
"failedToClearContacts": "Échec de l'effacement des contacts : {error}",
|
||||||
|
"failedToConnectViaSerial": "Échec de la connexion via série",
|
||||||
|
"failedToLoadPreviewImage": "Échec du chargement de l'aperçu : {error}",
|
||||||
|
"failedToLoadTrace": "Échec du chargement de la trace : {error}",
|
||||||
|
"failedToSendSarMarker": "Échec de l'envoi du marqueur SAR : {error}",
|
||||||
|
"failedToSetBuzzerMode": "Échec du réglage du mode buzzer : {error}",
|
||||||
|
"failedToSetGpsMode": "Échec du réglage du mode GPS : {error}",
|
||||||
|
"failedToSyncDeviceTime": "Échec de la synchronisation de l'heure de l'appareil : {error}",
|
||||||
|
"failedToWipeDeviceData": "Échec de l'effacement des données de l'appareil : {error}",
|
||||||
|
"fastGpsTargetChannel": "Canal cible GPS rapide",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Mises à jour rapides, seuils et permissions",
|
||||||
|
"fetchingCatalog": "Récupération du catalogue...",
|
||||||
|
"fifteenMinutes": "15 minutes",
|
||||||
|
"fifteenSeconds": "15 secondes",
|
||||||
|
"filterByDirection": "Filtrer par direction",
|
||||||
|
"finish": "Terminer",
|
||||||
|
"fiveMinutes": "5 minutes",
|
||||||
|
"fiveSeconds": "5 secondes",
|
||||||
|
"fix": "Correctif",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Politique d'annonce GPS",
|
||||||
|
"gpsAndLocation": "GPS et localisation",
|
||||||
|
"gpsDiagnostics": "Diagnostics GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Aide",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Masqué",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Comment la radio ajoute automatiquement les nœuds découverts.",
|
||||||
|
"images": "Images",
|
||||||
|
"importProfile": "Importer un profil",
|
||||||
|
"lastFix": "Dernier correctif",
|
||||||
|
"latest": "Le plus récent",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"loadASavedRegion": "Charger une région enregistrée",
|
||||||
|
"loading": "Chargement",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Partage de position, matériel et intervalle de mise à jour.",
|
||||||
|
"locationTelemetry": "Télémétrie de position",
|
||||||
|
"lockDestination": "Verrouiller la destination",
|
||||||
|
"longitude": "Longitude",
|
||||||
|
"mapStyle": "Style de carte",
|
||||||
|
"max": "Max",
|
||||||
|
"maxZoom": "Zoom max",
|
||||||
|
"messaging": "Messagerie",
|
||||||
|
"metHistory": "Historique MET",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Zoom min",
|
||||||
|
"moveDown": "Déplacer vers le bas",
|
||||||
|
"moveUp": "Déplacer vers le haut",
|
||||||
|
"multiAckMode": "Mode multi-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Gestion d'espace de travail multi-appareils",
|
||||||
|
"myLocation": "Ma position",
|
||||||
|
"nameAndTelemetryShared": "Nom et télémétrie partagés avec d'autres appareils.",
|
||||||
|
"newVersionAvailable": "Nouvelle version : {version}",
|
||||||
|
"newer": "Plus récent",
|
||||||
|
"noNeighboursFound": "Aucun voisin trouvé",
|
||||||
|
"noServersFound": "Aucun serveur trouvé",
|
||||||
|
"notSet": "Non défini",
|
||||||
|
"notifications": "Notifications",
|
||||||
|
"offZeroSeconds": "Désactivé (0 s)",
|
||||||
|
"offlineMaps": "Cartes hors ligne",
|
||||||
|
"older": "Plus ancien",
|
||||||
|
"oneByteMode0": "1 octet (mode 0)",
|
||||||
|
"oneHour": "1 heure",
|
||||||
|
"oneMinute": "1 minute",
|
||||||
|
"openPacketLogs": "Ouvrir les journaux de paquets",
|
||||||
|
"packetTypeHelp": "Aide sur les types de paquets",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Coller un lien de partage ou une annonce hex",
|
||||||
|
"pathHash": "Hash du chemin",
|
||||||
|
"pathSize": "Taille du chemin",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Polygone",
|
||||||
|
"rawResponseCopied": "Réponse brute copiée",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Renvoyer via le répéteur le plus proche en cas d'échec",
|
||||||
|
"resolveContact": "Résoudre le contact",
|
||||||
|
"routingRetriesAndDestinationLock": "Routage, tentatives et verrouillage de destination",
|
||||||
|
"samples": "Échantillons",
|
||||||
|
"satellites": "Satellites",
|
||||||
|
"saving": "Enregistrement...",
|
||||||
|
"search": "Rechercher",
|
||||||
|
"searchByNameOrToken": "Rechercher par nom ou jeton",
|
||||||
|
"searchDiscoveredNodes": "Rechercher les nœuds découverts",
|
||||||
|
"searchInCurrentFilter": "Rechercher dans le filtre actuel",
|
||||||
|
"searchLogs": "Rechercher dans les journaux...",
|
||||||
|
"searchSensors": "Rechercher les capteurs",
|
||||||
|
"searchThisSection": "Rechercher dans cette section",
|
||||||
|
"selectedRelays": "Relais sélectionnés",
|
||||||
|
"sensor": "Capteur",
|
||||||
|
"serialError": "Erreur série : {error}",
|
||||||
|
"setPathForContact": "Définir le chemin pour {name}",
|
||||||
|
"shareLiveGps": "Partager le GPS en direct",
|
||||||
|
"shareMyTiles": "Partager mes tuiles",
|
||||||
|
"sharingLocationFromPhone": "Partage de la position sur ce canal depuis le téléphone.",
|
||||||
|
"sharingLocationFromRadio": "Partage de la position sur ce canal depuis la radio.",
|
||||||
|
"signalDbm": "Signal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Taille, compression et aperçu",
|
||||||
|
"skip": "Passer",
|
||||||
|
"smallerFileSize": "Taille de fichier réduite",
|
||||||
|
"sort": "Trier",
|
||||||
|
"stoppedSharingLocation": "Arrêt du partage de position sur ce canal.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Supprimer les notifications en premier plan",
|
||||||
|
"tabsAndNavigation": "Onglets et navigation",
|
||||||
|
"templatesAndTutorials": "Modèles et tutoriels",
|
||||||
|
"tenMinutes": "10 minutes",
|
||||||
|
"tenSeconds": "10 secondes",
|
||||||
|
"testSendUpdate": "Tester l'envoi de mise à jour",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Thème, langue et préférences d'affichage",
|
||||||
|
"thirtyMinutes": "30 minutes",
|
||||||
|
"thirtySeconds": "30 secondes",
|
||||||
|
"threeBytesMode2": "3 octets (mode 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Statistiques de trafic, historique des messages et outils de développement",
|
||||||
|
"twoBytesMode1": "2 octets (mode 1)",
|
||||||
|
"twoMinutes": "2 minutes",
|
||||||
|
"undo": "Annuler",
|
||||||
|
"updated": "Mis à jour",
|
||||||
|
"useContactFlags": "Utiliser les drapeaux de contact",
|
||||||
|
"useSavedCoordinates": "Utiliser les coordonnées enregistrées",
|
||||||
|
"view": "Voir",
|
||||||
|
"viewNeighbours": "Voir les voisins",
|
||||||
|
"viewPublicStats": "Voir les statistiques publiques",
|
||||||
|
"setRegionScope": "Définir la portée régionale",
|
||||||
|
"regionScope": "Portée régionale",
|
||||||
|
"regionScopeNone": "Aucune (globale)",
|
||||||
|
"clearRegionScope": "Effacer la portée",
|
||||||
|
"regionScopeWarning": "Seuls les relais autorisant cette région retransmettront.",
|
||||||
|
"discoverRegions": "Découvrir depuis les relais",
|
||||||
|
"discoveringRegions": "Découverte des régions...",
|
||||||
|
"enterRegionName": "Nom de région (ex. auckland)",
|
||||||
|
"noRegionsFound": "Aucune région trouvée sur ce relais.",
|
||||||
|
"regionScopeSet": "Portée régionale définie sur {name}",
|
||||||
|
"regionScopeCleared": "Portée régionale effacée",
|
||||||
|
"simpleMode": "Mode simple",
|
||||||
|
"simpleModeDescription": "Uniquement le chat et la carte avec les contacts favoris",
|
||||||
|
"simpleModeRequiresLockedDestination": "Activez d'abord « Verrouiller la destination » avec un canal pour utiliser le mode simple",
|
||||||
|
"noFavouriteContacts": "Aucun contact favori",
|
||||||
|
"selectDestinationFirst": "Sélectionnez d'abord un canal, un contact ou un salon",
|
||||||
|
"recipientNoLongerAvailable": "Le destinataire n'est plus disponible",
|
||||||
|
"failedToSendMessage": "Échec de l'envoi : {error}",
|
||||||
|
"tictactoeDirectOnly": "Le morpion ne fonctionne qu'en messages directs. Choisissez d'abord un contact.",
|
||||||
|
"deviceKeyUnavailable": "La clé de l'appareil n'est pas disponible",
|
||||||
|
"imageCompressionFailed": "Échec de la compression de l'image",
|
||||||
|
"imageFragmentationFailed": "Échec de la fragmentation de l'image",
|
||||||
|
"failedToAnnounceImage": "Échec de l'annonce de l'image",
|
||||||
|
"noRecipientSelected": "Aucun destinataire sélectionné",
|
||||||
|
"imageSendFailed": "Échec de l'envoi de l'image",
|
||||||
|
"microphonePermissionRequiredForVoice": "L'autorisation du microphone est requise pour la voix",
|
||||||
|
"recordingDiscarded": "Enregistrement abandonné",
|
||||||
|
"publicChannelMediaWarning": "Vous êtes sur le point d'envoyer {mediaType} sur le canal public. Ce n'est pas conseillé, car tout le monde sur le réseau mesh peut le recevoir. Choisissez un canal privé ou étiqueté, sauf si c'est bien votre intention.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Sélectionnez un canal privé pour partager votre position.",
|
||||||
|
"stopSharingMyLocation": "Arrêter de partager ma position",
|
||||||
|
"shareMyLocation": "Partager ma position",
|
||||||
|
"stopRecording": "Arrêter l'enregistrement",
|
||||||
|
"recordVoice": "Enregistrer un message vocal",
|
||||||
|
"moreActions": "Plus d'actions",
|
||||||
|
"noMessagesInFilter": "Aucun message dans ce filtre",
|
||||||
|
"noMatchesInFilter": "Aucun résultat dans ce filtre",
|
||||||
|
"syncFailed": "Échec de la synchronisation : {error}",
|
||||||
|
"invalidLink": "Lien invalide",
|
||||||
|
"cannotOpenLink": "Impossible d'ouvrir le lien",
|
||||||
|
"failedToOpenLink": "Échec de l'ouverture du lien",
|
||||||
|
"failedToResendMessage": "Échec du renvoi du message",
|
||||||
|
"retryFailed": "Échec de la nouvelle tentative : {error}",
|
||||||
|
"deleteDrawingAndMessage": "Supprimer le dessin et le message",
|
||||||
|
"lastEchoRelay": "Dernier écho : relais",
|
||||||
|
"lastEchoPath": "Dernier écho : chemin",
|
||||||
|
"lastEchoBytesReport": "Dernier écho : rapport d'octets",
|
||||||
|
"noGpsCoordinatesAvailable": "Aucune coordonnée GPS disponible",
|
||||||
|
"notASarMarker": "Ce n'est pas un marqueur SAR",
|
||||||
|
"failedToSaveTemplate": "Échec de l'enregistrement du modèle : {error}",
|
||||||
|
"drawingNotFound": "Dessin introuvable",
|
||||||
|
"unknownDrawingType": "Type de dessin inconnu",
|
||||||
|
"customMapMarker": "Marqueur de carte personnalisé",
|
||||||
|
"customMapPointLabel": "Point : {point}",
|
||||||
|
"mapIdLabel": "ID de carte : {id}",
|
||||||
|
"cannotFetchVoice": "Impossible de récupérer la voix",
|
||||||
|
"senderContactUnknown": "Le contact de l'expéditeur est inconnu. Synchronisez d'abord les contacts.",
|
||||||
|
"senderRouteUnknown": "La route de l'expéditeur est inconnue. Synchronisez d'abord les contacts/le chemin.",
|
||||||
|
"messageTooFar": "Le message est trop loin ({hops} sauts, max. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "La route de l'expéditeur n'a pas répondu à la vérification du chemin. Synchronisez les contacts/le chemin et réessayez.",
|
||||||
|
"senderRouteNoRawResponse": "La route de l'expéditeur n'a pas répondu sur le chemin de transport brut.",
|
||||||
|
"voiceFetchOverHops": "La récupération de la voix sur {hops} sauts peut prendre un moment.",
|
||||||
|
"voiceReceiveCanceled": "Réception vocale annulée",
|
||||||
|
"fetchingMissingVoiceFragments": "Récupération des fragments vocaux manquants",
|
||||||
|
"receivingVoice": "Réception de la voix",
|
||||||
|
"cannotFetchImage": "Impossible de récupérer l'image",
|
||||||
|
"imageFetchOverHops": "La récupération de l'image sur {hops} sauts peut prendre un moment.",
|
||||||
|
"imageAlreadyBeingReceived": "L'image est déjà en cours de réception",
|
||||||
|
"loadImage": "Charger l'image",
|
||||||
|
"imageFetchFailedToSendRequest": "Échec de l'envoi de la demande d'image",
|
||||||
|
"imageUnavailable": "Image indisponible pour le moment",
|
||||||
|
"imageFetchTimedOut": "Délai de récupération de l'image dépassé",
|
||||||
|
"imageReceiveCanceled": "Réception de l'image annulée",
|
||||||
|
"closeImagePreview": "Fermer l'aperçu de l'image",
|
||||||
|
"recordingReleaseToSend": "Enregistrement... relâchez pour envoyer",
|
||||||
|
"sendingVoice": "Envoi du message vocal...",
|
||||||
|
"sendLongPressToRecordVoice": "Envoyer (appui long pour enregistrer la voix)",
|
||||||
|
"discardRecording": "Abandonner l'enregistrement",
|
||||||
|
"hopCount": "{count, plural, one{{count} saut} other{{count} sauts}}",
|
||||||
|
"channelsSectionDescription": "Canaux de diffusion pour le trafic mesh à proximité",
|
||||||
|
"roomsSectionDescription": "Espaces partagés pour la coordination d'équipe",
|
||||||
|
"contactsSectionDescription": "Personnes et appareils joignables directement",
|
||||||
|
"noRecentChatters": "Aucun participant récent",
|
||||||
|
"public": "Public",
|
||||||
|
"failedToSendTicTacToeMove": "Échec de l'envoi du coup de morpion",
|
||||||
|
"youWon": "Vous avez gagné",
|
||||||
|
"opponentWon": "L'adversaire a gagné",
|
||||||
|
"gameDraw": "Match nul",
|
||||||
|
"yourTurn": "À vous de jouer",
|
||||||
|
"opponentTurn": "Au tour de l'adversaire",
|
||||||
|
"pleaseEnterValidCoordinates": "Veuillez saisir des coordonnées valides",
|
||||||
|
"locationNotAvailable": "Position non disponible",
|
||||||
|
"customCaveMapPoint": "Point de carte de grotte personnalisé",
|
||||||
|
"mapPoint": "Point de carte",
|
||||||
|
"fetchingMissingImageFragments": "Récupération des fragments manquants…",
|
||||||
|
"loadingImage": "Chargement…",
|
||||||
|
"receivingImage": "Réception…",
|
||||||
|
"tapToLoad": "Touchez pour charger",
|
||||||
|
"connectDevice": "Connecter l'appareil",
|
||||||
|
"chooseTransportSubtitle": "Choisissez le transport Bluetooth, WiFi ou Série",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Réseau",
|
||||||
|
"serial": "Série",
|
||||||
|
"scan": "Rechercher",
|
||||||
|
"pressScanToSearchForDevices": "Appuyez sur Rechercher pour trouver les appareils à proximité",
|
||||||
|
"failedToConnectToDevice": "Échec de la connexion à {name}",
|
||||||
|
"unknownDevice": "Appareil inconnu",
|
||||||
|
"showingCachedResultsTapRefresh": "Affichage des résultats en cache. Touchez Actualiser pour relancer la recherche.",
|
||||||
|
"scanningLocalNetworkOnPort": "Recherche d'appareils MeshCore WiFi sur le réseau local, port {port}",
|
||||||
|
"cancelScanAndAddServer": "Annuler la recherche et ajouter un serveur",
|
||||||
|
"addServer": "Ajouter un serveur",
|
||||||
|
"scanningProgressIps": "Recherche... {scanned}/{total} IP",
|
||||||
|
"cancelAndEnterManually": "Annuler et saisir manuellement",
|
||||||
|
"noRecentOrDiscoveredServers": "Aucun serveur récent ou découvert pour le moment",
|
||||||
|
"recentlyUsed": "Utilisés récemment",
|
||||||
|
"connecting": "Connexion...",
|
||||||
|
"discoveredOnThisNetwork": "Découverts sur ce réseau",
|
||||||
|
"serverNoLongerAvailable": "Le serveur {host}:{port} n'est plus disponible. Veuillez relancer la recherche pour trouver des serveurs actifs.",
|
||||||
|
"failedToConnectToHost": "Échec de la connexion à {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Saisissez une adresse IP valide",
|
||||||
|
"enterValidTcpPort": "Saisissez un port TCP valide",
|
||||||
|
"ipAddress": "Adresse IP",
|
||||||
|
"tcpPort": "Port TCP",
|
||||||
|
"customServerPort": "Port de serveur personnalisé",
|
||||||
|
"chat": "Chat",
|
||||||
|
"restoringPreviousLink": "Restauration de la liaison précédente",
|
||||||
|
"noDeviceConnected": "Aucun appareil connecté",
|
||||||
|
"sendAdvert": "Envoyer mon contact",
|
||||||
|
"advertMode": "Envoyer mon contact",
|
||||||
|
"advertModeSubtitle": "Choisissez jusqu'où vos informations de contact doivent être diffusées.",
|
||||||
|
"floodAdvertSent": "Contact envoyé à travers le réseau mesh",
|
||||||
|
"directAdvertSent": "Contact envoyé aux appareils à proximité"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"disconnect": "Prekini",
|
"disconnect": "Prekini",
|
||||||
"noDevicesFound": "Nisu pronađeni uređaji",
|
"noDevicesFound": "Nisu pronađeni uređaji",
|
||||||
"scanAgain": "Skeniraj ponovno",
|
"scanAgain": "Skeniraj ponovno",
|
||||||
"tapToConnect": "Dodirnite za povezivanje",
|
|
||||||
"deviceNotConnected": "Uređaj nije povezan",
|
"deviceNotConnected": "Uređaj nije povezan",
|
||||||
"locationPermissionDenied": "Dopuštenje za lokaciju odbijeno",
|
"locationPermissionDenied": "Dopuštenje za lokaciju odbijeno",
|
||||||
"locationPermissionPermanentlyDenied": "Dopuštenje za lokaciju trajno odbijeno. Molimo omogućite u Postavkama.",
|
"locationPermissionPermanentlyDenied": "Dopuštenje za lokaciju trajno odbijeno. Molimo omogućite u Postavkama.",
|
||||||
@@ -32,9 +31,7 @@
|
|||||||
"autoSystem": "Automatski (Sustav)",
|
"autoSystem": "Automatski (Sustav)",
|
||||||
"followSystemTheme": "Slijedi temu sustava",
|
"followSystemTheme": "Slijedi temu sustava",
|
||||||
"showRxTxIndicators": "Prikaži RX/TX indikatore",
|
"showRxTxIndicators": "Prikaži RX/TX indikatore",
|
||||||
"displayPacketActivity": "Prikaži indikatore aktivnosti paketa u gornjoj traci",
|
|
||||||
"disableMap": "Onemogući kartu",
|
"disableMap": "Onemogući kartu",
|
||||||
"disableMapDescription": "Sakrij karticu karte za uštedu baterije",
|
|
||||||
"language": "Jezik",
|
"language": "Jezik",
|
||||||
"chooseLanguage": "Odaberite jezik",
|
"chooseLanguage": "Odaberite jezik",
|
||||||
"save": "Spremi",
|
"save": "Spremi",
|
||||||
@@ -42,7 +39,6 @@
|
|||||||
"close": "Zatvori",
|
"close": "Zatvori",
|
||||||
"about": "O aplikaciji",
|
"about": "O aplikaciji",
|
||||||
"appVersion": "Verzija aplikacije",
|
"appVersion": "Verzija aplikacije",
|
||||||
"appName": "Ime aplikacije",
|
|
||||||
"aboutMeshCoreSar": "O MeshCore SAR",
|
"aboutMeshCoreSar": "O MeshCore SAR",
|
||||||
"aboutDescription": "Aplikacija za potragu i spašavanje dizajnirana za timove za hitne slučajeve. Značajke uključuju:\n\n• BLE mesh mrežu za komunikaciju uređaj-uređaj\n• Offline karte s više slojeva\n• Praćenje članova tima u stvarnom vremenu\n• SAR taktički markeri (pronađena osoba, požar, zbirno mjesto)\n• Upravljanje kontaktima i razmjena poruka\n• GPS praćenje s kompasnim smjerom\n• Predmemoriranje karata za offline upotrebu",
|
"aboutDescription": "Aplikacija za potragu i spašavanje dizajnirana za timove za hitne slučajeve. Značajke uključuju:\n\n• BLE mesh mrežu za komunikaciju uređaj-uređaj\n• Offline karte s više slojeva\n• Praćenje članova tima u stvarnom vremenu\n• SAR taktički markeri (pronađena osoba, požar, zbirno mjesto)\n• Upravljanje kontaktima i razmjena poruka\n• GPS praćenje s kompasnim smjerom\n• Predmemoriranje karata za offline upotrebu",
|
||||||
"technologiesUsed": "Korištene tehnologije:",
|
"technologiesUsed": "Korištene tehnologije:",
|
||||||
@@ -69,7 +65,6 @@
|
|||||||
"viewOnMap": "Prikaži na karti",
|
"viewOnMap": "Prikaži na karti",
|
||||||
"refresh": "Osvježi",
|
"refresh": "Osvježi",
|
||||||
"resetPath": "Resetiraj put (preusmjeri)",
|
"resetPath": "Resetiraj put (preusmjeri)",
|
||||||
"publicKeyCopied": "Javni ključ kopiran u međuspremnik",
|
|
||||||
"copiedToClipboard": "{label} kopirano u međuspremnik",
|
"copiedToClipboard": "{label} kopirano u međuspremnik",
|
||||||
"pleaseEnterPassword": "Molimo unesite lozinku",
|
"pleaseEnterPassword": "Molimo unesite lozinku",
|
||||||
"failedToSyncContacts": "Neuspjela sinkronizacija kontakata: {error}",
|
"failedToSyncContacts": "Neuspjela sinkronizacija kontakata: {error}",
|
||||||
@@ -148,15 +143,9 @@
|
|||||||
"type": "Vrsta",
|
"type": "Vrsta",
|
||||||
"publicKey": "Javni ključ",
|
"publicKey": "Javni ključ",
|
||||||
"lastSeen": "Zadnje viđen",
|
"lastSeen": "Zadnje viđen",
|
||||||
"roomStatus": "Status sobe",
|
|
||||||
"loginStatus": "Status prijave",
|
|
||||||
"notLoggedIn": "Nije prijavljen",
|
|
||||||
"adminAccess": "Administratorski pristup",
|
|
||||||
"yes": "Da",
|
"yes": "Da",
|
||||||
"no": "Ne",
|
"no": "Ne",
|
||||||
"permissions": "Dopuštenja",
|
"permissions": "Dopuštenja",
|
||||||
"passwordSaved": "Lozinka spremljena",
|
|
||||||
"locationColon": "Lokacija:",
|
|
||||||
"telemetry": "Telemetrija",
|
"telemetry": "Telemetrija",
|
||||||
"voltage": "Napon",
|
"voltage": "Napon",
|
||||||
"battery": "Baterija",
|
"battery": "Baterija",
|
||||||
@@ -164,8 +153,6 @@
|
|||||||
"humidity": "Vlažnost",
|
"humidity": "Vlažnost",
|
||||||
"pressure": "Tlak",
|
"pressure": "Tlak",
|
||||||
"gpsTelemetry": "GPS (Telemetrija)",
|
"gpsTelemetry": "GPS (Telemetrija)",
|
||||||
"updated": "Ažurirano",
|
|
||||||
"pathResetInfo": "Put resetiran za {name}. Sljedeća poruka će pronaći novu rutu.",
|
|
||||||
"reLoginToRoom": "Ponovna prijava u sobu",
|
"reLoginToRoom": "Ponovna prijava u sobu",
|
||||||
"heading": "Smjer",
|
"heading": "Smjer",
|
||||||
"elevation": "Nadmorska visina",
|
"elevation": "Nadmorska visina",
|
||||||
@@ -240,7 +227,6 @@
|
|||||||
"tapToViewOnMap": "Dodirnite za prikaz na karti",
|
"tapToViewOnMap": "Dodirnite za prikaz na karti",
|
||||||
"radioSettings": "Postavke radija",
|
"radioSettings": "Postavke radija",
|
||||||
"frequencyMHz": "Frekvencija (MHz)",
|
"frequencyMHz": "Frekvencija (MHz)",
|
||||||
"frequencyExample": "npr. 869.618",
|
|
||||||
"bandwidth": "Širina pojasa",
|
"bandwidth": "Širina pojasa",
|
||||||
"spreadingFactor": "Faktor širenja",
|
"spreadingFactor": "Faktor širenja",
|
||||||
"codingRate": "Omjer kodiranja",
|
"codingRate": "Omjer kodiranja",
|
||||||
@@ -263,38 +249,20 @@
|
|||||||
"noTrailRecorded": "Još nije snimljena putanja",
|
"noTrailRecorded": "Još nije snimljena putanja",
|
||||||
"startTrackingToRecord": "Pokrenite praćenje lokacije za snimanje putanje",
|
"startTrackingToRecord": "Pokrenite praćenje lokacije za snimanje putanje",
|
||||||
"trailControls": "Upravljanje putanjom",
|
"trailControls": "Upravljanje putanjom",
|
||||||
"contactTrails": "Putanje kontakata",
|
|
||||||
"showAllContactTrails": "Prikaži sve putanje kontakata",
|
|
||||||
"noContactsWithLocationHistory": "Nema kontakata s poviješću lokacije",
|
|
||||||
"showingTrailsForContacts": "Prikazujem putanje za {count} kontakata",
|
|
||||||
"individualContactTrails": "Pojedinačne putanje kontakata",
|
|
||||||
"deviceInformation": "Informacije o uređaju",
|
|
||||||
"bleName": "BLE naziv",
|
|
||||||
"meshName": "Mesh naziv",
|
|
||||||
"notSet": "Nije postavljeno",
|
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"version": "Verzija",
|
"version": "Verzija",
|
||||||
"buildDate": "Datum izgradnje",
|
|
||||||
"firmware": "Firmver",
|
|
||||||
"maxContacts": "Maks. kontakata",
|
"maxContacts": "Maks. kontakata",
|
||||||
"maxChannels": "Maks. kanala",
|
"maxChannels": "Maks. kanala",
|
||||||
"publicInfo": "Javne informacije",
|
"publicInfo": "Javne informacije",
|
||||||
"meshNetworkName": "Naziv mesh mreže",
|
|
||||||
"nameBroadcastInMesh": "Naziv koji se emitira u mesh oglasima",
|
|
||||||
"telemetryAndLocationSharing": "Telemetrija i dijeljenje lokacije",
|
|
||||||
"lat": "Šir",
|
|
||||||
"lon": "Duž",
|
|
||||||
"useCurrentLocation": "Koristi trenutnu lokaciju",
|
"useCurrentLocation": "Koristi trenutnu lokaciju",
|
||||||
"noneUnknown": "Nema/Nepoznato",
|
"noneUnknown": "Nema/Nepoznato",
|
||||||
"chatNode": "Čvorište za razgovor",
|
"chatNode": "Čvorište za razgovor",
|
||||||
"repeater": "Repetitor",
|
"repeater": "Repetitor",
|
||||||
"roomChannel": "Soba/Kanal",
|
"roomChannel": "Soba/Kanal",
|
||||||
"typeNumber": "Tip {number}",
|
"typeNumber": "Tip {number}",
|
||||||
"copiedToClipboardShort": "Kopirano {label} u međuspremnik",
|
|
||||||
"failedToSave": "Neuspjelo spremanje: {error}",
|
"failedToSave": "Neuspjelo spremanje: {error}",
|
||||||
"failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}",
|
"failedToGetLocation": "Neuspjelo dohvaćanje lokacije: {error}",
|
||||||
"sarTemplates": "SAR predlošci",
|
"sarTemplates": "SAR predlošci",
|
||||||
"manageSarTemplates": "Upravljanje SAR predlošcima",
|
|
||||||
"addTemplate": "Dodaj predložak",
|
"addTemplate": "Dodaj predložak",
|
||||||
"editTemplate": "Uredi predložak",
|
"editTemplate": "Uredi predložak",
|
||||||
"deleteTemplate": "Izbriši predložak",
|
"deleteTemplate": "Izbriši predložak",
|
||||||
@@ -322,7 +290,6 @@
|
|||||||
"noTemplates": "Nema dostupnih predložaka",
|
"noTemplates": "Nema dostupnih predložaka",
|
||||||
"tapAddToCreate": "Dodirnite + za izradu prvog predloška",
|
"tapAddToCreate": "Dodirnite + za izradu prvog predloška",
|
||||||
"ok": "U redu",
|
"ok": "U redu",
|
||||||
"permissionsSection": "Dozvole",
|
|
||||||
"locationPermission": "Dozvola za lokaciju",
|
"locationPermission": "Dozvola za lokaciju",
|
||||||
"checking": "Provjera...",
|
"checking": "Provjera...",
|
||||||
"locationPermissionGrantedAlways": "Odobreno (Uvijek)",
|
"locationPermissionGrantedAlways": "Odobreno (Uvijek)",
|
||||||
@@ -460,28 +427,6 @@
|
|||||||
"wizardNext": "Dalje",
|
"wizardNext": "Dalje",
|
||||||
"wizardGetStarted": "Započni",
|
"wizardGetStarted": "Započni",
|
||||||
"wizardWelcomeTitle": "Dobrodošli u MeshCore SAR",
|
"wizardWelcomeTitle": "Dobrodošli u MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Moćan alat za komunikaciju izvan mreže za spasilačke operacije. Povežite se s timom uz mesh radijsku tehnologiju kada tradicionalne mreže nisu dostupne.",
|
|
||||||
"wizardConnectingTitle": "Povezivanje s radiom",
|
|
||||||
"wizardConnectingDescription": "Povežite telefon s MeshCore radijskim uređajem putem Bluetootha i započnite komunikaciju izvan mreže.",
|
|
||||||
"wizardConnectingFeature1": "Skenira obližnje MeshCore uređaje",
|
|
||||||
"wizardConnectingFeature2": "Uparivanje s radijem putem Bluetootha",
|
|
||||||
"wizardConnectingFeature3": "Radi potpuno izvan mreže — internet nije potreban",
|
|
||||||
"wizardChannelTitle": "Kanali",
|
|
||||||
"wizardChannelDescription": "Šaljite poruke svima na kanalu — idealno za obavijesti i koordinaciju tima.",
|
|
||||||
"wizardChannelFeature1": "Javni kanal za opću komunikaciju ekipe",
|
|
||||||
"wizardChannelFeature2": "Stvorite prilagođene kanale za specifične grupe",
|
|
||||||
"wizardChannelFeature3": "Poruke se automatski prosljeđuju putem mreže",
|
|
||||||
"wizardContactsTitle": "Kontakti",
|
|
||||||
"wizardContactsDescription": "Članovi tima se prikazuju automatski kada se pridruže mesh mreži. Šaljite im izravne poruke ili pogledajte njihovu lokaciju.",
|
|
||||||
"wizardContactsFeature1": "Kontakti se automatski otkrivaju",
|
|
||||||
"wizardContactsFeature2": "Šaljite privatne direktne poruke",
|
|
||||||
"wizardContactsFeature3": "Prikažite stanje baterije i vrijeme zadnje aktivnosti",
|
|
||||||
"wizardMapTitle": "Karta i lokacija",
|
|
||||||
"wizardMapDescription": "Pratite tim u stvarnom vremenu i označavajte ključne lokacije za spasilačke operacije.",
|
|
||||||
"wizardMapFeature1": "SAR oznake za pronađene osobe, požare i točke okupljanja",
|
|
||||||
"wizardMapFeature2": "GPS praćenje članova tima u stvarnom vremenu",
|
|
||||||
"wizardMapFeature3": "Preuzmite karte za rad izvan mreže",
|
|
||||||
"wizardMapFeature4": "Crtajte oblike i dijelite taktičke informacije",
|
|
||||||
"viewWelcomeTutorial": "Pogledaj uputu dobrodošlice",
|
"viewWelcomeTutorial": "Pogledaj uputu dobrodošlice",
|
||||||
"allTeamContacts": "Svi kontakti tima",
|
"allTeamContacts": "Svi kontakti tima",
|
||||||
"directMessagesInfo": "Izravne poruke s potvrdom. Poslano {count} članovima tima.",
|
"directMessagesInfo": "Izravne poruke s potvrdom. Poslano {count} članovima tima.",
|
||||||
@@ -563,5 +508,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Aktivno",
|
||||||
|
"addContact": "Dodaj kontakt",
|
||||||
|
"all": "Sve",
|
||||||
|
"clearAllLabel": "Očisti sve",
|
||||||
|
"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",
|
||||||
|
"trace": "Praćenje",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"hideRepeatersOnMap": "Sakrij posrednike na karti",
|
||||||
|
"setMapScale": "Postavi mjerilo karte",
|
||||||
|
"customMapScaleSaved": "Prilagođeno mjerilo karte spremljeno",
|
||||||
|
"voiceBitrate": "Bitna brzina glasa",
|
||||||
|
"voiceCompressor": "Kompresor glasa",
|
||||||
|
"voiceLimiter": "Ograničivač glasa",
|
||||||
|
"micAutoGain": "Automatsko pojačanje mikrofona",
|
||||||
|
"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",
|
||||||
|
"busy": "Zauzeto",
|
||||||
|
"searchMessages": "Pretraži poruke",
|
||||||
|
"sendImageFromGallery": "Pošalji sliku iz galerije",
|
||||||
|
"takePhoto": "Snimi fotografiju",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Automatsko slanje",
|
||||||
|
"autoSendDescription": "Pošalji trenutnom rutom.",
|
||||||
|
"sendDirect": "Izravno slanje",
|
||||||
|
"sendDirectDescription": "Pošalji izravno ovom kontaktu.",
|
||||||
|
"sendFlood": "Slanje poplavom",
|
||||||
|
"sendFloodDescription": "Pošalji preko svih repetitora.",
|
||||||
|
"postConnectDiscoveryTitle": "Otkriti repetitore sada?",
|
||||||
|
"postConnectDiscoveryDescription": "Pokrenite otkrivanje repetitora odmah nakon povezivanja kako biste vidjeli obližnje MeshCore čvorove i brže ih dodali u svoju mrežu.",
|
||||||
|
"aToZ": "A-Ž",
|
||||||
|
"add": "Dodaj",
|
||||||
|
"addAdditionalDetails": "Dodaj dodatne detalje",
|
||||||
|
"addPeer": "Dodaj čvor",
|
||||||
|
"addPeerManually": "Ručno dodaj čvor",
|
||||||
|
"addedToContacts": "{name} dodan u kontakte",
|
||||||
|
"addedToSensors": "{name} dodan u Senzore",
|
||||||
|
"allowAll": "Dopusti sve",
|
||||||
|
"anonymousRxStats": "Anonimna RX statistika",
|
||||||
|
"appearance": "Izgled",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Osnovna telemetrija",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Zvučna upozorenja",
|
||||||
|
"cancelImageReceive": "Otkaži primanje slike",
|
||||||
|
"choose": "Odaberi",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Odaberite koje kartice i odjeljke kontakata prikazati",
|
||||||
|
"clearCache": "Očisti predmemoriju",
|
||||||
|
"clearHistory": "Očisti povijest",
|
||||||
|
"clearLiveView": "Očisti prikaz uživo",
|
||||||
|
"clearLogs": "Očisti zapise",
|
||||||
|
"clearOfflineCache": "Očistiti offline predmemoriju?",
|
||||||
|
"clearOverlay": "Očisti prekrivanje",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Očisti rutu samo nakon što svi pokušaji ne uspiju",
|
||||||
|
"codecBitrateAndAudioProcessing": "Kodek, bitna brzina i obrada zvuka",
|
||||||
|
"connectByIpAddress": "Poveži se putem IP adrese",
|
||||||
|
"controlWhichAlertsYouReceive": "Upravljajte upozorenjima koja primate",
|
||||||
|
"copyFullHex": "Kopiraj cijeli Hex",
|
||||||
|
"copyRawResponse": "Kopiraj sirovi odgovor",
|
||||||
|
"couldNotLoadMetHistory": "Nije moguće učitati MET povijest",
|
||||||
|
"customizeSensor": "Prilagodi {name}",
|
||||||
|
"data": "Podaci",
|
||||||
|
"deleteGroup": "Izbriši grupu",
|
||||||
|
"deleteStyleConfirm": "Izbrisati {name}?",
|
||||||
|
"deleteThisDrawing": "Izbrisati ovaj {type}?",
|
||||||
|
"deny": "Odbij",
|
||||||
|
"deviceInfo": "Informacije o uređaju",
|
||||||
|
"deviceSettings": "Postavke uređaja",
|
||||||
|
"deviceTimeSynced": "Vrijeme uređaja sinkronizirano s ovim telefonom.",
|
||||||
|
"disableFastGpsPublishing": "Onemogući brzo GPS objavljivanje",
|
||||||
|
"displayMarkersAndTraceDatabase": "Prikaz, markeri i baza tragova",
|
||||||
|
"download": "Preuzmi",
|
||||||
|
"environmentalTelemetry": "Okolišna telemetrija",
|
||||||
|
"errorCheckingForUpdates": "Greška pri provjeri ažuriranja: {error}",
|
||||||
|
"errorGeneric": "Greška: {error}",
|
||||||
|
"errorOpeningDownload": "Greška pri otvaranju preuzimanja: {error}",
|
||||||
|
"exportLogs": "Izvezi zapise",
|
||||||
|
"extraAggressiveAvifCompression": "Dodatno agresivna AVIF kompresija",
|
||||||
|
"failedToAddContact": "Neuspjelo dodavanje kontakta: {error}",
|
||||||
|
"failedToAddName": "Neuspjelo dodavanje {name}: {error}",
|
||||||
|
"failedToClearChannels": "Neuspjelo brisanje kanala: {error}",
|
||||||
|
"failedToClearContacts": "Neuspjelo brisanje kontakata: {error}",
|
||||||
|
"failedToConnectViaSerial": "Neuspjelo povezivanje putem serijskog porta",
|
||||||
|
"failedToLoadPreviewImage": "Neuspjelo učitavanje slike za pregled: {error}",
|
||||||
|
"failedToLoadTrace": "Neuspjelo učitavanje traga: {error}",
|
||||||
|
"failedToSendSarMarker": "Neuspjelo slanje SAR markera: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Neuspjelo postavljanje načina zujalice: {error}",
|
||||||
|
"failedToSetGpsMode": "Neuspjelo postavljanje GPS načina: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Neuspjela sinkronizacija vremena uređaja: {error}",
|
||||||
|
"failedToWipeDeviceData": "Neuspjelo brisanje podataka uređaja: {error}",
|
||||||
|
"fastGpsTargetChannel": "Ciljni kanal brzog GPS-a",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Brza ažuriranja, pragovi i dopuštenja",
|
||||||
|
"fetchingCatalog": "Dohvaćanje kataloga...",
|
||||||
|
"fifteenMinutes": "15 minuta",
|
||||||
|
"fifteenSeconds": "15 sekundi",
|
||||||
|
"filterByDirection": "Filtriraj po smjeru",
|
||||||
|
"finish": "Završi",
|
||||||
|
"fiveMinutes": "5 minuta",
|
||||||
|
"fiveSeconds": "5 sekundi",
|
||||||
|
"fix": "Fiksiranje",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Pravila GPS oglašavanja",
|
||||||
|
"gpsAndLocation": "GPS i lokacija",
|
||||||
|
"gpsDiagnostics": "GPS dijagnostika",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Pomoć",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Skriveno",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Kako radio automatski dodaje otkrivene čvorove.",
|
||||||
|
"images": "Slike",
|
||||||
|
"importProfile": "Uvezi profil",
|
||||||
|
"lastFix": "Zadnje fiksiranje",
|
||||||
|
"latest": "Najnovije",
|
||||||
|
"latitude": "Geografska širina",
|
||||||
|
"loadASavedRegion": "Učitaj spremljenu regiju",
|
||||||
|
"loading": "Učitavanje",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Dijeljenje lokacije, hardver i interval ažuriranja.",
|
||||||
|
"locationTelemetry": "Telemetrija lokacije",
|
||||||
|
"lockDestination": "Zaključaj odredište",
|
||||||
|
"longitude": "Geografska dužina",
|
||||||
|
"mapStyle": "Stil karte",
|
||||||
|
"max": "Maks",
|
||||||
|
"maxZoom": "Maks. zumiranje",
|
||||||
|
"messaging": "Poruke",
|
||||||
|
"metHistory": "MET povijest",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Min. zumiranje",
|
||||||
|
"moveDown": "Pomakni dolje",
|
||||||
|
"moveUp": "Pomakni gore",
|
||||||
|
"multiAckMode": "Multi-ACK način",
|
||||||
|
"multiDeviceWorkspaceManagement": "Upravljanje radnim prostorom s više uređaja",
|
||||||
|
"myLocation": "Moja lokacija",
|
||||||
|
"nameAndTelemetryShared": "Ime i telemetrija dijeljeni s drugim uređajima.",
|
||||||
|
"newVersionAvailable": "Nova verzija: {version}",
|
||||||
|
"newer": "Novije",
|
||||||
|
"noNeighboursFound": "Nisu pronađeni susjedi",
|
||||||
|
"noServersFound": "Nisu pronađeni poslužitelji",
|
||||||
|
"notSet": "Nije postavljeno",
|
||||||
|
"notifications": "Obavijesti",
|
||||||
|
"offZeroSeconds": "Isključeno (0 s)",
|
||||||
|
"offlineMaps": "Offline karte",
|
||||||
|
"older": "Starije",
|
||||||
|
"oneByteMode0": "1 bajt (način 0)",
|
||||||
|
"oneHour": "1 sat",
|
||||||
|
"oneMinute": "1 minuta",
|
||||||
|
"openPacketLogs": "Otvori zapise paketa",
|
||||||
|
"packetTypeHelp": "Pomoć za vrstu paketa",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Zalijepite dijeljenu vezu ili Hex oglas",
|
||||||
|
"pathHash": "Hash putanje",
|
||||||
|
"pathSize": "Veličina putanje",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Poligon",
|
||||||
|
"rawResponseCopied": "Sirovi odgovor kopiran",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Ponovno pošalji kroz najbliži posrednik pri neuspjehu",
|
||||||
|
"resolveContact": "Razriješi kontakt",
|
||||||
|
"routingRetriesAndDestinationLock": "Usmjeravanje, pokušaji i zaključavanje odredišta",
|
||||||
|
"samples": "Uzorci",
|
||||||
|
"satellites": "Sateliti",
|
||||||
|
"saving": "Spremanje...",
|
||||||
|
"search": "Pretraži",
|
||||||
|
"searchByNameOrToken": "Pretraži po imenu ili tokenu",
|
||||||
|
"searchDiscoveredNodes": "Pretraži otkrivene čvorove",
|
||||||
|
"searchInCurrentFilter": "Pretraži u trenutnom filtru",
|
||||||
|
"searchLogs": "Pretraži zapise...",
|
||||||
|
"searchSensors": "Pretraži senzore",
|
||||||
|
"searchThisSection": "Pretraži ovaj odjeljak",
|
||||||
|
"selectedRelays": "Odabrani posrednici",
|
||||||
|
"sensor": "Senzor",
|
||||||
|
"serialError": "Serijska greška: {error}",
|
||||||
|
"setPathForContact": "Postavi putanju za {name}",
|
||||||
|
"shareLiveGps": "Dijeli GPS uživo",
|
||||||
|
"shareMyTiles": "Dijeli moje pločice",
|
||||||
|
"sharingLocationFromPhone": "Dijeljenje lokacije na ovom kanalu s telefona.",
|
||||||
|
"sharingLocationFromRadio": "Dijeljenje lokacije na ovom kanalu s radija.",
|
||||||
|
"signalDbm": "Signal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Veličina, kompresija i pregled",
|
||||||
|
"skip": "Preskoči",
|
||||||
|
"smallerFileSize": "Manja veličina datoteke",
|
||||||
|
"sort": "Sortiraj",
|
||||||
|
"stoppedSharingLocation": "Zaustavljeno dijeljenje lokacije na ovom kanalu.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Utiši obavijesti dok je aplikacija u prvom planu",
|
||||||
|
"tabsAndNavigation": "Kartice i navigacija",
|
||||||
|
"templatesAndTutorials": "Predlošci i upute",
|
||||||
|
"tenMinutes": "10 minuta",
|
||||||
|
"tenSeconds": "10 sekundi",
|
||||||
|
"testSendUpdate": "Testno slanje ažuriranja",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, jezik i postavke prikaza",
|
||||||
|
"thirtyMinutes": "30 minuta",
|
||||||
|
"thirtySeconds": "30 sekundi",
|
||||||
|
"threeBytesMode2": "3 bajta (način 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Statistika prometa, povijest poruka i razvojni alati",
|
||||||
|
"twoBytesMode1": "2 bajta (način 1)",
|
||||||
|
"twoMinutes": "2 minute",
|
||||||
|
"undo": "Poništi",
|
||||||
|
"updated": "Ažurirano",
|
||||||
|
"useContactFlags": "Koristi zastavice kontakata",
|
||||||
|
"useSavedCoordinates": "Koristi spremljene koordinate",
|
||||||
|
"view": "Prikaži",
|
||||||
|
"viewNeighbours": "Prikaži susjede",
|
||||||
|
"viewPublicStats": "Prikaži javnu statistiku",
|
||||||
|
"setRegionScope": "Postavi opseg regije",
|
||||||
|
"regionScope": "Opseg regije",
|
||||||
|
"regionScopeNone": "Nema (globalno)",
|
||||||
|
"clearRegionScope": "Obriši opseg",
|
||||||
|
"regionScopeWarning": "Samo repetitori koji dopuštaju ovu regiju će prosljeđivati.",
|
||||||
|
"discoverRegions": "Otkrij iz repetitora",
|
||||||
|
"discoveringRegions": "Otkrivanje regija...",
|
||||||
|
"enterRegionName": "Naziv regije (npr. auckland)",
|
||||||
|
"noRegionsFound": "Nema pronađenih regija na ovom repetitoru.",
|
||||||
|
"regionScopeSet": "Opseg regije postavljen na {name}",
|
||||||
|
"regionScopeCleared": "Opseg regije obrisan",
|
||||||
|
"simpleMode": "Jednostavni način",
|
||||||
|
"simpleModeDescription": "Samo razgovor i karta s omiljenim kontaktima",
|
||||||
|
"simpleModeRequiresLockedDestination": "Za korištenje jednostavnog načina najprije omogućite „Zaključaj odredište” s kanalom",
|
||||||
|
"noFavouriteContacts": "Nema omiljenih kontakata",
|
||||||
|
"selectDestinationFirst": "Najprije odaberite kanal, kontakt ili sobu",
|
||||||
|
"recipientNoLongerAvailable": "Primatelj više nije dostupan",
|
||||||
|
"failedToSendMessage": "Slanje nije uspjelo: {error}",
|
||||||
|
"tictactoeDirectOnly": "Križić-kružić radi samo u izravnim porukama. Najprije odaberite kontakt.",
|
||||||
|
"deviceKeyUnavailable": "Ključ uređaja nije dostupan",
|
||||||
|
"imageCompressionFailed": "Komprimiranje slike nije uspjelo",
|
||||||
|
"imageFragmentationFailed": "Fragmentiranje slike nije uspjelo",
|
||||||
|
"failedToAnnounceImage": "Najava slike nije uspjela",
|
||||||
|
"noRecipientSelected": "Primatelj nije odabran",
|
||||||
|
"imageSendFailed": "Slanje slike nije uspjelo",
|
||||||
|
"microphonePermissionRequiredForVoice": "Za glasovne poruke potrebna je dozvola za mikrofon",
|
||||||
|
"recordingDiscarded": "Snimka je odbačena",
|
||||||
|
"publicChannelMediaWarning": "Upravo ćete poslati {mediaType} na javni kanal. To se ne preporučuje jer sadržaj mogu primiti svi na mesh mreži. Odaberite privatni ili označeni kanal, osim ako je to baš ono što želite.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Odaberite privatni kanal za dijeljenje lokacije.",
|
||||||
|
"stopSharingMyLocation": "Prestani dijeliti moju lokaciju",
|
||||||
|
"shareMyLocation": "Dijeli moju lokaciju",
|
||||||
|
"stopRecording": "Zaustavi snimanje",
|
||||||
|
"recordVoice": "Snimi glasovnu poruku",
|
||||||
|
"moreActions": "Više radnji",
|
||||||
|
"noMessagesInFilter": "Nema poruka u ovom filtru",
|
||||||
|
"noMatchesInFilter": "Nema rezultata u ovom filtru",
|
||||||
|
"syncFailed": "Sinkronizacija nije uspjela: {error}",
|
||||||
|
"invalidLink": "Nevažeća poveznica",
|
||||||
|
"cannotOpenLink": "Poveznicu nije moguće otvoriti",
|
||||||
|
"failedToOpenLink": "Otvaranje poveznice nije uspjelo",
|
||||||
|
"failedToResendMessage": "Ponovno slanje poruke nije uspjelo",
|
||||||
|
"retryFailed": "Ponovni pokušaj nije uspio: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Izbriši crtež i poruku",
|
||||||
|
"lastEchoRelay": "Zadnji odjek – relej",
|
||||||
|
"lastEchoPath": "Zadnji odjek – putanja",
|
||||||
|
"lastEchoBytesReport": "Zadnji odjek – izvještaj o bajtovima",
|
||||||
|
"noGpsCoordinatesAvailable": "GPS koordinate nisu dostupne",
|
||||||
|
"notASarMarker": "Nije SAR marker",
|
||||||
|
"failedToSaveTemplate": "Spremanje predloška nije uspjelo: {error}",
|
||||||
|
"drawingNotFound": "Crtež nije pronađen",
|
||||||
|
"unknownDrawingType": "Nepoznata vrsta crteža",
|
||||||
|
"customMapMarker": "Prilagođeni marker karte",
|
||||||
|
"customMapPointLabel": "Točka: {point}",
|
||||||
|
"mapIdLabel": "ID karte: {id}",
|
||||||
|
"cannotFetchVoice": "Glasovnu poruku nije moguće dohvatiti",
|
||||||
|
"senderContactUnknown": "Kontakt pošiljatelja nije poznat. Najprije sinkronizirajte kontakte.",
|
||||||
|
"senderRouteUnknown": "Ruta pošiljatelja nije poznata. Najprije sinkronizirajte kontakte/putanju.",
|
||||||
|
"messageTooFar": "Poruka je predaleko ({hops} skokova, najviše {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Ruta pošiljatelja nije odgovorila na provjeru putanje. Sinkronizirajte kontakte/putanju i pokušajte ponovno.",
|
||||||
|
"senderRouteNoRawResponse": "Ruta pošiljatelja nije odgovorila na izravnoj prijenosnoj putanji.",
|
||||||
|
"voiceFetchOverHops": "Dohvaćanje glasa preko {hops} skokova može potrajati.",
|
||||||
|
"voiceReceiveCanceled": "Primanje glasa je otkazano",
|
||||||
|
"fetchingMissingVoiceFragments": "Dohvaćanje nedostajućih glasovnih fragmenata",
|
||||||
|
"receivingVoice": "Primanje glasa",
|
||||||
|
"cannotFetchImage": "Sliku nije moguće dohvatiti",
|
||||||
|
"imageFetchOverHops": "Dohvaćanje slike preko {hops} skokova može potrajati.",
|
||||||
|
"imageAlreadyBeingReceived": "Slika se već prima",
|
||||||
|
"loadImage": "Učitaj sliku",
|
||||||
|
"imageFetchFailedToSendRequest": "Zahtjev za dohvaćanje slike nije poslan",
|
||||||
|
"imageUnavailable": "Slika trenutačno nije dostupna",
|
||||||
|
"imageFetchTimedOut": "Dohvaćanje slike je isteklo",
|
||||||
|
"imageReceiveCanceled": "Primanje slike je otkazano",
|
||||||
|
"closeImagePreview": "Zatvori pregled slike",
|
||||||
|
"recordingReleaseToSend": "Snimanje... otpustite za slanje glasa",
|
||||||
|
"sendingVoice": "Slanje glasa...",
|
||||||
|
"sendLongPressToRecordVoice": "Pošalji (dugi pritisak za snimanje glasa)",
|
||||||
|
"discardRecording": "Odbaci snimku",
|
||||||
|
"hopCount": "{count, plural, one{{count} skok} few{{count} skoka} other{{count} skokova}}",
|
||||||
|
"channelsSectionDescription": "Kanali za emitiranje mesh prometa u blizini",
|
||||||
|
"roomsSectionDescription": "Zajednički prostori za stalnu koordinaciju tima",
|
||||||
|
"contactsSectionDescription": "Osobe i uređaji koje možete izravno dosegnuti",
|
||||||
|
"noRecentChatters": "Nema nedavnih sudionika",
|
||||||
|
"public": "Javno",
|
||||||
|
"failedToSendTicTacToeMove": "Potez križić-kružić nije poslan",
|
||||||
|
"youWon": "Pobijedili ste",
|
||||||
|
"opponentWon": "Protivnik je pobijedio",
|
||||||
|
"gameDraw": "Neriješeno",
|
||||||
|
"yourTurn": "Vi ste na potezu",
|
||||||
|
"opponentTurn": "Protivnik je na potezu",
|
||||||
|
"pleaseEnterValidCoordinates": "Unesite valjane koordinate",
|
||||||
|
"locationNotAvailable": "Lokacija nije dostupna",
|
||||||
|
"customCaveMapPoint": "Prilagođena točka karte špilje",
|
||||||
|
"mapPoint": "Točka karte",
|
||||||
|
"fetchingMissingImageFragments": "Dohvaćanje nedostajućih fragmenata…",
|
||||||
|
"loadingImage": "Učitavanje…",
|
||||||
|
"receivingImage": "Primanje…",
|
||||||
|
"tapToLoad": "Dodirnite za učitavanje",
|
||||||
|
"connectDevice": "Poveži uređaj",
|
||||||
|
"chooseTransportSubtitle": "Odaberite način povezivanja: Bluetooth, WiFi ili serijski",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Mreža",
|
||||||
|
"serial": "Serijski",
|
||||||
|
"scan": "Skeniraj",
|
||||||
|
"pressScanToSearchForDevices": "Pritisnite Skeniraj za traženje uređaja u blizini",
|
||||||
|
"failedToConnectToDevice": "Povezivanje s uređajem {name} nije uspjelo",
|
||||||
|
"unknownDevice": "Nepoznat uređaj",
|
||||||
|
"showingCachedResultsTapRefresh": "Prikazani su predmemorirani rezultati. Dodirnite osvježi za ponovno skeniranje.",
|
||||||
|
"scanningLocalNetworkOnPort": "Skeniranje lokalne mreže za MeshCore WiFi uređaje na portu {port}",
|
||||||
|
"cancelScanAndAddServer": "Otkaži skeniranje i dodaj poslužitelj",
|
||||||
|
"addServer": "Dodaj poslužitelj",
|
||||||
|
"scanningProgressIps": "Skeniranje... {scanned}/{total} IP adresa",
|
||||||
|
"cancelAndEnterManually": "Otkaži i unesi ručno",
|
||||||
|
"noRecentOrDiscoveredServers": "Još nema nedavnih ili otkrivenih poslužitelja",
|
||||||
|
"recentlyUsed": "Nedavno korišteno",
|
||||||
|
"connecting": "Povezivanje...",
|
||||||
|
"discoveredOnThisNetwork": "Otkriveno na ovoj mreži",
|
||||||
|
"serverNoLongerAvailable": "Poslužitelj na {host}:{port} više nije dostupan. Skenirajte ponovno da pronađete aktivne poslužitelje.",
|
||||||
|
"failedToConnectToHost": "Povezivanje s {host}:{port} nije uspjelo",
|
||||||
|
"enterValidIpAddress": "Unesite valjanu IP adresu",
|
||||||
|
"enterValidTcpPort": "Unesite valjani TCP port",
|
||||||
|
"ipAddress": "IP adresa",
|
||||||
|
"tcpPort": "TCP port",
|
||||||
|
"customServerPort": "Prilagođeni port poslužitelja",
|
||||||
|
"chat": "Razgovor",
|
||||||
|
"restoringPreviousLink": "Obnavljanje prethodne veze",
|
||||||
|
"noDeviceConnected": "Nijedan uređaj nije povezan",
|
||||||
|
"sendAdvert": "Pošalji moj kontakt",
|
||||||
|
"advertMode": "Pošalji moj kontakt",
|
||||||
|
"advertModeSubtitle": "Odaberite koliko daleko trebaju putovati vaši kontaktni podaci.",
|
||||||
|
"floodAdvertSent": "Kontakt poslan preplavljanjem kroz mrežu",
|
||||||
|
"directAdvertSent": "Kontakt poslan izravno obližnjim uređajima"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Pulsante per riavviare la ricerca BLE"
|
"description": "Pulsante per riavviare la ricerca BLE"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Tocca per connettere",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Testo sottotitolo per dispositivo nell'elenco di ricerca"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Dispositivo non connesso",
|
"deviceNotConnected": "Dispositivo non connesso",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Messaggio di errore quando il dispositivo non è connesso"
|
"description": "Messaggio di errore quando il dispositivo non è connesso"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Impostazione per mostrare gli indicatori RX/TX"
|
"description": "Impostazione per mostrare gli indicatori RX/TX"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Mostra indicatori di attività pacchetti nella barra superiore",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Descrizione per l'impostazione degli indicatori RX/TX"
|
|
||||||
},
|
|
||||||
"disableMap": "Disabilita mappa",
|
"disableMap": "Disabilita mappa",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Impostazione per disabilitare la scheda mappa"
|
"description": "Impostazione per disabilitare la scheda mappa"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Nascondi la scheda mappa per ridurre il consumo della batteria",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Descrizione per l'impostazione di disabilitazione mappa"
|
|
||||||
},
|
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Etichetta impostazione lingua"
|
"description": "Etichetta impostazione lingua"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "Etichetta versione app"
|
"description": "Etichetta versione app"
|
||||||
},
|
},
|
||||||
"appName": "Nome App",
|
|
||||||
"@appName": {
|
|
||||||
"description": "Etichetta nome app"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Informazioni su MeshCore SAR",
|
"aboutMeshCoreSar": "Informazioni su MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "Titolo della finestra Informazioni"
|
"description": "Titolo della finestra Informazioni"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Azione per resettare il percorso del contatto per il re-instradamento"
|
"description": "Azione per resettare il percorso del contatto per il re-instradamento"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Chiave pubblica copiata negli appunti",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Messaggio di successo quando la chiave pubblica viene copiata"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} copiato negli appunti",
|
"copiedToClipboard": "{label} copiato negli appunti",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Messaggio di successo quando un valore viene copiato negli appunti",
|
"description": "Messaggio di successo quando un valore viene copiato negli appunti",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Etichetta ultima visita"
|
"description": "Etichetta ultima visita"
|
||||||
},
|
},
|
||||||
"roomStatus": "Stato Stanza",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Intestazione sezione stato stanza"
|
|
||||||
},
|
|
||||||
"loginStatus": "Stato Accesso",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Etichetta stato accesso"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Non Connesso",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Stato non connesso"
|
|
||||||
},
|
|
||||||
"adminAccess": "Accesso Admin",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Etichetta accesso amministratore"
|
|
||||||
},
|
|
||||||
"yes": "Sì",
|
"yes": "Sì",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Risposta Sì"
|
"description": "Risposta Sì"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Etichetta permessi"
|
"description": "Etichetta permessi"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Password Salvata",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Etichetta password salvata"
|
|
||||||
},
|
|
||||||
"locationColon": "Posizione:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Intestazione sezione posizione"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Intestazione sezione telemetria"
|
"description": "Intestazione sezione telemetria"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "Etichetta GPS da telemetria"
|
"description": "Etichetta GPS da telemetria"
|
||||||
},
|
},
|
||||||
"updated": "Aggiornato",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Etichetta timestamp aggiornato"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Percorso resettato per {name}. Il prossimo messaggio troverà un nuovo instradamento.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Messaggio informativo dopo il reset del percorso",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Riaccedi alla Stanza",
|
"reLoginToRoom": "Riaccedi alla Stanza",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Pulsante per riaccedere alla stanza"
|
"description": "Pulsante per riaccedere alla stanza"
|
||||||
@@ -1114,10 +1057,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Etichetta per il campo frequenza radio"
|
"description": "Etichetta per il campo frequenza radio"
|
||||||
},
|
},
|
||||||
"frequencyExample": "es., 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Testo di aiuto esempio per la frequenza"
|
|
||||||
},
|
|
||||||
"bandwidth": "Larghezza di Banda",
|
"bandwidth": "Larghezza di Banda",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Etichetta per il menu a discesa larghezza di banda"
|
"description": "Etichetta per il menu a discesa larghezza di banda"
|
||||||
@@ -1221,47 +1160,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Tooltip controlli traccia"
|
"description": "Tooltip controlli traccia"
|
||||||
},
|
},
|
||||||
"contactTrails": "Tracce contatti",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Intestazione sezione tracce contatti"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Mostra tutte le tracce dei contatti",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Etichetta dell'interruttore per mostrare tutte le tracce dei contatti"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Nessun contatto con cronologia posizione",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Sottotitolo quando non ci sono contatti con tracce"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Visualizzazione tracce per {count} contatti",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Sottotitolo che mostra il numero di contatti con tracce",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Tracce individuali dei contatti",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Titolo dell'elemento espandibile per le tracce individuali dei contatti"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Informazioni Dispositivo",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Intestazione sezione informazioni dispositivo"
|
|
||||||
},
|
|
||||||
"bleName": "Nome BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Etichetta nome dispositivo Bluetooth Low Energy"
|
|
||||||
},
|
|
||||||
"meshName": "Nome Mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Etichetta nome rete mesh"
|
|
||||||
},
|
|
||||||
"notSet": "Non impostato",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Etichetta quando un valore non è impostato"
|
|
||||||
},
|
|
||||||
"model": "Modello",
|
"model": "Modello",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Etichetta modello dispositivo"
|
"description": "Etichetta modello dispositivo"
|
||||||
@@ -1270,14 +1168,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Etichetta versione"
|
"description": "Etichetta versione"
|
||||||
},
|
},
|
||||||
"buildDate": "Data Build",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Etichetta data build firmware"
|
|
||||||
},
|
|
||||||
"firmware": "Firmware",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Etichetta firmware"
|
|
||||||
},
|
|
||||||
"maxContacts": "Contatti Max",
|
"maxContacts": "Contatti Max",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Etichetta capacità massima contatti"
|
"description": "Etichetta capacità massima contatti"
|
||||||
@@ -1290,26 +1180,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Intestazione sezione informazioni pubbliche"
|
"description": "Intestazione sezione informazioni pubbliche"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Nome Rete Mesh",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Etichetta campo nome rete mesh"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Nome trasmesso negli annunci mesh",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Testo di aiuto per il campo nome rete mesh"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetria e Condivisione Posizione",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Etichetta interruttore telemetria e condivisione posizione"
|
|
||||||
},
|
|
||||||
"lat": "Lat",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Etichetta campo latitudine (forma breve)"
|
|
||||||
},
|
|
||||||
"lon": "Lon",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Etichetta campo longitudine (forma breve)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Usa posizione attuale",
|
"useCurrentLocation": "Usa posizione attuale",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip per il pulsante usa posizione attuale"
|
"description": "Tooltip per il pulsante usa posizione attuale"
|
||||||
@@ -1339,15 +1209,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "Copiato {label} negli appunti",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Messaggio di successo breve quando si copia negli appunti",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Impossibile salvare: {error}",
|
"failedToSave": "Impossibile salvare: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Messaggio di errore generico per fallimenti di salvataggio",
|
"description": "Messaggio di errore generico per fallimenti di salvataggio",
|
||||||
@@ -1367,7 +1228,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sarTemplates": "Modelli SAR",
|
"sarTemplates": "Modelli SAR",
|
||||||
"manageSarTemplates": "Gestisci modelli SAR",
|
|
||||||
"addTemplate": "Aggiungi modello",
|
"addTemplate": "Aggiungi modello",
|
||||||
"editTemplate": "Modifica modello",
|
"editTemplate": "Modifica modello",
|
||||||
"deleteTemplate": "Elimina modello",
|
"deleteTemplate": "Elimina modello",
|
||||||
@@ -1395,7 +1255,6 @@
|
|||||||
"noTemplates": "Nessun modello disponibile",
|
"noTemplates": "Nessun modello disponibile",
|
||||||
"tapAddToCreate": "Tocca + per creare il tuo primo modello",
|
"tapAddToCreate": "Tocca + per creare il tuo primo modello",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"permissionsSection": "Permessi",
|
|
||||||
"locationPermission": "Permesso di posizione",
|
"locationPermission": "Permesso di posizione",
|
||||||
"checking": "Verifica in corso...",
|
"checking": "Verifica in corso...",
|
||||||
"locationPermissionGrantedAlways": "Concesso (Sempre)",
|
"locationPermissionGrantedAlways": "Concesso (Sempre)",
|
||||||
@@ -1538,28 +1397,6 @@
|
|||||||
"wizardNext": "Avanti",
|
"wizardNext": "Avanti",
|
||||||
"wizardGetStarted": "Inizia",
|
"wizardGetStarted": "Inizia",
|
||||||
"wizardWelcomeTitle": "Benvenuto in MeshCore SAR",
|
"wizardWelcomeTitle": "Benvenuto in MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Un potente strumento di comunicazione offline per operazioni di ricerca e soccorso. Connettiti con il tuo team usando la tecnologia radio mesh quando le reti tradizionali non sono disponibili.",
|
|
||||||
"wizardConnectingTitle": "Connessione alla Radio",
|
|
||||||
"wizardConnectingDescription": "Collega il tuo smartphone a un dispositivo radio MeshCore tramite Bluetooth per iniziare a comunicare offline.",
|
|
||||||
"wizardConnectingFeature1": "Cerca dispositivi MeshCore nelle vicinanze",
|
|
||||||
"wizardConnectingFeature2": "Accoppia con la tua radio tramite Bluetooth",
|
|
||||||
"wizardConnectingFeature3": "Funziona completamente offline - non è richiesta connessione internet",
|
|
||||||
"wizardChannelTitle": "Canali",
|
|
||||||
"wizardChannelDescription": "Trasmetti messaggi a tutti su un canale, perfetto per annunci e coordinamento di tutto il team.",
|
|
||||||
"wizardChannelFeature1": "Canale pubblico per comunicazione generale del team",
|
|
||||||
"wizardChannelFeature2": "Crea canali personalizzati per gruppi specifici",
|
|
||||||
"wizardChannelFeature3": "I messaggi vengono automaticamente inoltrati attraverso la rete mesh",
|
|
||||||
"wizardContactsTitle": "Contatti",
|
|
||||||
"wizardContactsDescription": "I membri del tuo team appaiono automaticamente quando si uniscono alla rete mesh. Invia loro messaggi diretti o visualizza la loro posizione.",
|
|
||||||
"wizardContactsFeature1": "Contatti scoperti automaticamente",
|
|
||||||
"wizardContactsFeature2": "Invia messaggi diretti privati",
|
|
||||||
"wizardContactsFeature3": "Visualizza livello batteria e ultima volta visto",
|
|
||||||
"wizardMapTitle": "Mappa & Posizione",
|
|
||||||
"wizardMapDescription": "Traccia il tuo team in tempo reale e segna posizioni importanti per operazioni di ricerca e soccorso.",
|
|
||||||
"wizardMapFeature1": "Marcatori SAR per persone trovate, incendi e aree di staging",
|
|
||||||
"wizardMapFeature2": "Tracciamento GPS in tempo reale dei membri del team",
|
|
||||||
"wizardMapFeature3": "Scarica mappe offline per aree remote",
|
|
||||||
"wizardMapFeature4": "Disegna forme e condividi informazioni tattiche",
|
|
||||||
"viewWelcomeTutorial": "Visualizza tutorial di benvenuto",
|
"viewWelcomeTutorial": "Visualizza tutorial di benvenuto",
|
||||||
"allTeamContacts": "Tutti i contatti del team",
|
"allTeamContacts": "Tutti i contatti del team",
|
||||||
"directMessagesInfo": "Messaggi diretti con conferme. Inviato a {count} membri del team.",
|
"directMessagesInfo": "Messaggi diretti con conferme. Inviato a {count} membri del team.",
|
||||||
@@ -1641,5 +1478,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Attivo",
|
||||||
|
"addContact": "Aggiungi contatto",
|
||||||
|
"all": "Tutto",
|
||||||
|
"clearAllLabel": "Cancella tutto",
|
||||||
|
"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",
|
||||||
|
"trace": "Traccia",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"hideRepeatersOnMap": "Nascondi ripetitori sulla mappa",
|
||||||
|
"setMapScale": "Imposta scala mappa",
|
||||||
|
"customMapScaleSaved": "Scala mappa personalizzata salvata",
|
||||||
|
"voiceBitrate": "Bitrate voce",
|
||||||
|
"voiceCompressor": "Compressore voce",
|
||||||
|
"voiceLimiter": "Limitatore voce",
|
||||||
|
"micAutoGain": "Guadagno automatico microfono",
|
||||||
|
"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",
|
||||||
|
"busy": "Occupato",
|
||||||
|
"searchMessages": "Cerca messaggi",
|
||||||
|
"sendImageFromGallery": "Invia immagine dalla galleria",
|
||||||
|
"takePhoto": "Scatta foto",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Invio automatico",
|
||||||
|
"autoSendDescription": "Invia tramite il percorso attuale.",
|
||||||
|
"sendDirect": "Invio diretto",
|
||||||
|
"sendDirectDescription": "Invia direttamente a questo contatto.",
|
||||||
|
"sendFlood": "Invio flood",
|
||||||
|
"sendFloodDescription": "Invia tramite tutti i ripetitori.",
|
||||||
|
"postConnectDiscoveryTitle": "Scoprire i ripetitori ora?",
|
||||||
|
"postConnectDiscoveryDescription": "Esegui la scoperta dei ripetitori subito dopo la connessione per vedere i nodi MeshCore nelle vicinanze e aggiungerli alla tua rete più velocemente.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Aggiungi",
|
||||||
|
"addAdditionalDetails": "Aggiungi dettagli aggiuntivi",
|
||||||
|
"addPeer": "Aggiungi peer",
|
||||||
|
"addPeerManually": "Aggiungi peer manualmente",
|
||||||
|
"addedToContacts": "{name} aggiunto ai contatti",
|
||||||
|
"addedToSensors": "{name} aggiunto ai sensori",
|
||||||
|
"allowAll": "Consenti tutto",
|
||||||
|
"anonymousRxStats": "Statistiche RX anonime",
|
||||||
|
"appearance": "Aspetto",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Telemetria di base",
|
||||||
|
"blePin": "PIN BLE",
|
||||||
|
"buzzerAlerts": "Avvisi buzzer",
|
||||||
|
"cancelImageReceive": "Annulla ricezione immagine",
|
||||||
|
"choose": "Scegli",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Scegli quali schede e sezioni contatti mostrare",
|
||||||
|
"clearCache": "Svuota cache",
|
||||||
|
"clearHistory": "Cancella cronologia",
|
||||||
|
"clearLiveView": "Cancella vista live",
|
||||||
|
"clearLogs": "Cancella log",
|
||||||
|
"clearOfflineCache": "Svuotare la cache offline?",
|
||||||
|
"clearOverlay": "Cancella sovrapposizione",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Cancella il percorso solo dopo che tutti i tentativi falliscono",
|
||||||
|
"codecBitrateAndAudioProcessing": "Codec, bitrate ed elaborazione audio",
|
||||||
|
"connectByIpAddress": "Connetti tramite indirizzo IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Controlla quali avvisi ricevi",
|
||||||
|
"copyFullHex": "Copia Hex completo",
|
||||||
|
"copyRawResponse": "Copia risposta grezza",
|
||||||
|
"couldNotLoadMetHistory": "Impossibile caricare la cronologia MET",
|
||||||
|
"customizeSensor": "Personalizza {name}",
|
||||||
|
"data": "Dati",
|
||||||
|
"deleteGroup": "Elimina gruppo",
|
||||||
|
"deleteStyleConfirm": "Eliminare {name}?",
|
||||||
|
"deleteThisDrawing": "Eliminare questo {type}?",
|
||||||
|
"deny": "Nega",
|
||||||
|
"deviceInfo": "Info dispositivo",
|
||||||
|
"deviceSettings": "Impostazioni dispositivo",
|
||||||
|
"deviceTimeSynced": "Ora del dispositivo sincronizzata con questo telefono.",
|
||||||
|
"disableFastGpsPublishing": "Disabilita pubblicazione GPS rapida",
|
||||||
|
"displayMarkersAndTraceDatabase": "Visualizzazione, marcatori e database tracce",
|
||||||
|
"download": "Scarica",
|
||||||
|
"environmentalTelemetry": "Telemetria ambientale",
|
||||||
|
"errorCheckingForUpdates": "Errore durante il controllo degli aggiornamenti: {error}",
|
||||||
|
"errorGeneric": "Errore: {error}",
|
||||||
|
"errorOpeningDownload": "Errore nell'apertura del download: {error}",
|
||||||
|
"exportLogs": "Esporta log",
|
||||||
|
"extraAggressiveAvifCompression": "Compressione AVIF extra aggressiva",
|
||||||
|
"failedToAddContact": "Impossibile aggiungere il contatto: {error}",
|
||||||
|
"failedToAddName": "Impossibile aggiungere {name}: {error}",
|
||||||
|
"failedToClearChannels": "Impossibile cancellare i canali: {error}",
|
||||||
|
"failedToClearContacts": "Impossibile cancellare i contatti: {error}",
|
||||||
|
"failedToConnectViaSerial": "Impossibile connettersi tramite seriale",
|
||||||
|
"failedToLoadPreviewImage": "Impossibile caricare l'anteprima: {error}",
|
||||||
|
"failedToLoadTrace": "Impossibile caricare la traccia: {error}",
|
||||||
|
"failedToSendSarMarker": "Impossibile inviare il marcatore SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Impossibile impostare la modalità buzzer: {error}",
|
||||||
|
"failedToSetGpsMode": "Impossibile impostare la modalità GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Impossibile sincronizzare l'ora del dispositivo: {error}",
|
||||||
|
"failedToWipeDeviceData": "Impossibile cancellare i dati del dispositivo: {error}",
|
||||||
|
"fastGpsTargetChannel": "Canale target GPS rapido",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Aggiornamenti rapidi, soglie e permessi",
|
||||||
|
"fetchingCatalog": "Recupero catalogo...",
|
||||||
|
"fifteenMinutes": "15 minuti",
|
||||||
|
"fifteenSeconds": "15 secondi",
|
||||||
|
"filterByDirection": "Filtra per direzione",
|
||||||
|
"finish": "Fine",
|
||||||
|
"fiveMinutes": "5 minuti",
|
||||||
|
"fiveSeconds": "5 secondi",
|
||||||
|
"fix": "Fix",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Politica annunci GPS",
|
||||||
|
"gpsAndLocation": "GPS e posizione",
|
||||||
|
"gpsDiagnostics": "Diagnostica GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Aiuto",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Nascosto",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Come la radio aggiunge automaticamente i nodi scoperti.",
|
||||||
|
"images": "Immagini",
|
||||||
|
"importProfile": "Importa profilo",
|
||||||
|
"lastFix": "Ultimo fix",
|
||||||
|
"latest": "Più recente",
|
||||||
|
"latitude": "Latitudine",
|
||||||
|
"loadASavedRegion": "Carica una regione salvata",
|
||||||
|
"loading": "Caricamento",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Condivisione posizione, hardware e intervallo di aggiornamento.",
|
||||||
|
"locationTelemetry": "Telemetria posizione",
|
||||||
|
"lockDestination": "Blocca destinazione",
|
||||||
|
"longitude": "Longitudine",
|
||||||
|
"mapStyle": "Stile mappa",
|
||||||
|
"max": "Max",
|
||||||
|
"maxZoom": "Zoom max",
|
||||||
|
"messaging": "Messaggistica",
|
||||||
|
"metHistory": "Cronologia MET",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Zoom min",
|
||||||
|
"moveDown": "Sposta giù",
|
||||||
|
"moveUp": "Sposta su",
|
||||||
|
"multiAckMode": "Modalità multi-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Gestione spazio di lavoro multi-dispositivo",
|
||||||
|
"myLocation": "La mia posizione",
|
||||||
|
"nameAndTelemetryShared": "Nome e telemetria condivisi con altri dispositivi.",
|
||||||
|
"newVersionAvailable": "Nuova versione: {version}",
|
||||||
|
"newer": "Più recente",
|
||||||
|
"noNeighboursFound": "Nessun vicino trovato",
|
||||||
|
"noServersFound": "Nessun server trovato",
|
||||||
|
"notSet": "Non impostato",
|
||||||
|
"notifications": "Notifiche",
|
||||||
|
"offZeroSeconds": "Off (0 s)",
|
||||||
|
"offlineMaps": "Mappe offline",
|
||||||
|
"older": "Meno recente",
|
||||||
|
"oneByteMode0": "1 byte (modo 0)",
|
||||||
|
"oneHour": "1 ora",
|
||||||
|
"oneMinute": "1 minuto",
|
||||||
|
"openPacketLogs": "Apri log pacchetti",
|
||||||
|
"packetTypeHelp": "Guida tipi di pacchetto",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Incolla un link di condivisione o annuncio Hex",
|
||||||
|
"pathHash": "Hash percorso",
|
||||||
|
"pathSize": "Dimensione percorso",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Poligono",
|
||||||
|
"rawResponseCopied": "Risposta grezza copiata",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Rinvia tramite il ripetitore più vicino in caso di errore",
|
||||||
|
"resolveContact": "Risolvi contatto",
|
||||||
|
"routingRetriesAndDestinationLock": "Instradamento, tentativi e blocco destinazione",
|
||||||
|
"samples": "Campioni",
|
||||||
|
"satellites": "Satelliti",
|
||||||
|
"saving": "Salvataggio...",
|
||||||
|
"search": "Cerca",
|
||||||
|
"searchByNameOrToken": "Cerca per nome o token",
|
||||||
|
"searchDiscoveredNodes": "Cerca nodi scoperti",
|
||||||
|
"searchInCurrentFilter": "Cerca nel filtro corrente",
|
||||||
|
"searchLogs": "Cerca nei log...",
|
||||||
|
"searchSensors": "Cerca sensori",
|
||||||
|
"searchThisSection": "Cerca in questa sezione",
|
||||||
|
"selectedRelays": "Ripetitori selezionati",
|
||||||
|
"sensor": "Sensore",
|
||||||
|
"serialError": "Errore seriale: {error}",
|
||||||
|
"setPathForContact": "Imposta percorso per {name}",
|
||||||
|
"shareLiveGps": "Condividi GPS in tempo reale",
|
||||||
|
"shareMyTiles": "Condividi le mie tile",
|
||||||
|
"sharingLocationFromPhone": "Condivisione posizione su questo canale dal telefono.",
|
||||||
|
"sharingLocationFromRadio": "Condivisione posizione su questo canale dalla radio.",
|
||||||
|
"signalDbm": "Segnale {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Dimensione, compressione e anteprima",
|
||||||
|
"skip": "Salta",
|
||||||
|
"smallerFileSize": "Dimensione file ridotta",
|
||||||
|
"sort": "Ordina",
|
||||||
|
"stoppedSharingLocation": "Condivisione posizione interrotta su questo canale.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Disattiva notifiche quando l'app è in primo piano",
|
||||||
|
"tabsAndNavigation": "Schede e navigazione",
|
||||||
|
"templatesAndTutorials": "Modelli e tutorial",
|
||||||
|
"tenMinutes": "10 minuti",
|
||||||
|
"tenSeconds": "10 secondi",
|
||||||
|
"testSendUpdate": "Prova invio aggiornamento",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, lingua e preferenze di visualizzazione",
|
||||||
|
"thirtyMinutes": "30 minuti",
|
||||||
|
"thirtySeconds": "30 secondi",
|
||||||
|
"threeBytesMode2": "3 byte (modo 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Statistiche traffico, cronologia messaggi e strumenti sviluppatore",
|
||||||
|
"twoBytesMode1": "2 byte (modo 1)",
|
||||||
|
"twoMinutes": "2 minuti",
|
||||||
|
"undo": "Annulla",
|
||||||
|
"updated": "Aggiornato",
|
||||||
|
"useContactFlags": "Usa flag contatti",
|
||||||
|
"useSavedCoordinates": "Usa coordinate salvate",
|
||||||
|
"view": "Visualizza",
|
||||||
|
"viewNeighbours": "Visualizza vicini",
|
||||||
|
"viewPublicStats": "Visualizza statistiche pubbliche",
|
||||||
|
"setRegionScope": "Imposta ambito regionale",
|
||||||
|
"regionScope": "Ambito regionale",
|
||||||
|
"regionScopeNone": "Nessuno (globale)",
|
||||||
|
"clearRegionScope": "Cancella ambito",
|
||||||
|
"regionScopeWarning": "Solo i ripetitori che consentono questa regione inoltreranno.",
|
||||||
|
"discoverRegions": "Scopri dai ripetitori",
|
||||||
|
"discoveringRegions": "Scoperta delle regioni...",
|
||||||
|
"enterRegionName": "Nome regione (es. auckland)",
|
||||||
|
"noRegionsFound": "Nessuna regione trovata su questo ripetitore.",
|
||||||
|
"regionScopeSet": "Ambito regionale impostato su {name}",
|
||||||
|
"regionScopeCleared": "Ambito regionale cancellato",
|
||||||
|
"simpleMode": "Modalità semplice",
|
||||||
|
"simpleModeDescription": "Solo chat e mappa con i contatti preferiti",
|
||||||
|
"simpleModeRequiresLockedDestination": "Attiva prima «Blocca destinazione» con un canale per usare la modalità semplice",
|
||||||
|
"noFavouriteContacts": "Nessun contatto preferito",
|
||||||
|
"selectDestinationFirst": "Seleziona prima un canale, un contatto o una stanza",
|
||||||
|
"recipientNoLongerAvailable": "Il destinatario non è più disponibile",
|
||||||
|
"failedToSendMessage": "Invio non riuscito: {error}",
|
||||||
|
"tictactoeDirectOnly": "Il tris funziona solo nei messaggi diretti. Scegli prima un contatto.",
|
||||||
|
"deviceKeyUnavailable": "La chiave del dispositivo non è disponibile",
|
||||||
|
"imageCompressionFailed": "Compressione dell'immagine non riuscita",
|
||||||
|
"imageFragmentationFailed": "Frammentazione dell'immagine non riuscita",
|
||||||
|
"failedToAnnounceImage": "Annuncio dell'immagine non riuscito",
|
||||||
|
"noRecipientSelected": "Nessun destinatario selezionato",
|
||||||
|
"imageSendFailed": "Invio dell'immagine non riuscito",
|
||||||
|
"microphonePermissionRequiredForVoice": "Per i messaggi vocali è necessario il permesso del microfono",
|
||||||
|
"recordingDiscarded": "Registrazione scartata",
|
||||||
|
"publicChannelMediaWarning": "Stai per inviare {mediaType} al Canale Pubblico. Non è consigliato, perché chiunque sulla rete mesh potrebbe riceverlo. Scegli un canale privato o con tag, a meno che non sia ciò che vuoi.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Seleziona un canale privato per condividere la tua posizione.",
|
||||||
|
"stopSharingMyLocation": "Smetti di condividere la mia posizione",
|
||||||
|
"shareMyLocation": "Condividi la mia posizione",
|
||||||
|
"stopRecording": "Interrompi registrazione",
|
||||||
|
"recordVoice": "Registra messaggio vocale",
|
||||||
|
"moreActions": "Altre azioni",
|
||||||
|
"noMessagesInFilter": "Nessun messaggio in questo filtro",
|
||||||
|
"noMatchesInFilter": "Nessuna corrispondenza in questo filtro",
|
||||||
|
"syncFailed": "Sincronizzazione non riuscita: {error}",
|
||||||
|
"invalidLink": "Link non valido",
|
||||||
|
"cannotOpenLink": "Impossibile aprire il link",
|
||||||
|
"failedToOpenLink": "Apertura del link non riuscita",
|
||||||
|
"failedToResendMessage": "Reinvio del messaggio non riuscito",
|
||||||
|
"retryFailed": "Nuovo tentativo non riuscito: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Elimina disegno e messaggio",
|
||||||
|
"lastEchoRelay": "Ultimo eco: relay",
|
||||||
|
"lastEchoPath": "Ultimo eco: percorso",
|
||||||
|
"lastEchoBytesReport": "Ultimo eco: report byte",
|
||||||
|
"noGpsCoordinatesAvailable": "Nessuna coordinata GPS disponibile",
|
||||||
|
"notASarMarker": "Non è un marcatore SAR",
|
||||||
|
"failedToSaveTemplate": "Salvataggio del modello non riuscito: {error}",
|
||||||
|
"drawingNotFound": "Disegno non trovato",
|
||||||
|
"unknownDrawingType": "Tipo di disegno sconosciuto",
|
||||||
|
"customMapMarker": "Marcatore mappa personalizzato",
|
||||||
|
"customMapPointLabel": "Punto: {point}",
|
||||||
|
"mapIdLabel": "ID mappa: {id}",
|
||||||
|
"cannotFetchVoice": "Impossibile recuperare il vocale",
|
||||||
|
"senderContactUnknown": "Il contatto del mittente è sconosciuto. Sincronizza prima i contatti.",
|
||||||
|
"senderRouteUnknown": "Il percorso del mittente è sconosciuto. Sincronizza prima contatti/percorso.",
|
||||||
|
"messageTooFar": "Il messaggio è troppo lontano ({hops} hop, max {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Il percorso del mittente non ha risposto alla verifica. Sincronizza contatti/percorso e riprova.",
|
||||||
|
"senderRouteNoRawResponse": "Il percorso del mittente non ha risposto sul percorso di trasporto raw.",
|
||||||
|
"voiceFetchOverHops": "Il recupero del vocale su {hops} hop può richiedere un po' di tempo.",
|
||||||
|
"voiceReceiveCanceled": "Ricezione vocale annullata",
|
||||||
|
"fetchingMissingVoiceFragments": "Recupero dei frammenti vocali mancanti",
|
||||||
|
"receivingVoice": "Ricezione vocale",
|
||||||
|
"cannotFetchImage": "Impossibile recuperare l'immagine",
|
||||||
|
"imageFetchOverHops": "Il recupero dell'immagine su {hops} hop può richiedere un po' di tempo.",
|
||||||
|
"imageAlreadyBeingReceived": "L'immagine è già in ricezione",
|
||||||
|
"loadImage": "Carica immagine",
|
||||||
|
"imageFetchFailedToSendRequest": "Invio della richiesta immagine non riuscito",
|
||||||
|
"imageUnavailable": "Immagine al momento non disponibile",
|
||||||
|
"imageFetchTimedOut": "Recupero dell'immagine scaduto",
|
||||||
|
"imageReceiveCanceled": "Ricezione dell'immagine annullata",
|
||||||
|
"closeImagePreview": "Chiudi anteprima immagine",
|
||||||
|
"recordingReleaseToSend": "Registrazione... rilascia per inviare",
|
||||||
|
"sendingVoice": "Invio vocale...",
|
||||||
|
"sendLongPressToRecordVoice": "Invia (pressione lunga per registrare un vocale)",
|
||||||
|
"discardRecording": "Scarta registrazione",
|
||||||
|
"hopCount": "{count, plural, =1{1 hop} other{{count} hop}}",
|
||||||
|
"channelsSectionDescription": "Canali broadcast per il traffico mesh nelle vicinanze",
|
||||||
|
"roomsSectionDescription": "Spazi condivisi per il coordinamento continuo del team",
|
||||||
|
"contactsSectionDescription": "Persone e dispositivi raggiungibili direttamente",
|
||||||
|
"noRecentChatters": "Nessun partecipante recente",
|
||||||
|
"public": "Pubblico",
|
||||||
|
"failedToSendTicTacToeMove": "Invio della mossa di tris non riuscito",
|
||||||
|
"youWon": "Hai vinto",
|
||||||
|
"opponentWon": "Ha vinto l'avversario",
|
||||||
|
"gameDraw": "Pareggio",
|
||||||
|
"yourTurn": "Tocca a te",
|
||||||
|
"opponentTurn": "Tocca all'avversario",
|
||||||
|
"pleaseEnterValidCoordinates": "Inserisci coordinate valide",
|
||||||
|
"locationNotAvailable": "Posizione non disponibile",
|
||||||
|
"customCaveMapPoint": "Punto mappa grotta personalizzato",
|
||||||
|
"mapPoint": "Punto mappa",
|
||||||
|
"fetchingMissingImageFragments": "Recupero frammenti mancanti…",
|
||||||
|
"loadingImage": "Caricamento…",
|
||||||
|
"receivingImage": "Ricezione…",
|
||||||
|
"tapToLoad": "Tocca per caricare",
|
||||||
|
"connectDevice": "Connetti dispositivo",
|
||||||
|
"chooseTransportSubtitle": "Scegli il trasporto Bluetooth, WiFi o Seriale",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Rete",
|
||||||
|
"serial": "Seriale",
|
||||||
|
"scan": "Cerca",
|
||||||
|
"pressScanToSearchForDevices": "Premi Cerca per trovare i dispositivi nelle vicinanze",
|
||||||
|
"failedToConnectToDevice": "Impossibile connettersi a {name}",
|
||||||
|
"unknownDevice": "Dispositivo sconosciuto",
|
||||||
|
"showingCachedResultsTapRefresh": "Risultati in cache. Tocca Aggiorna per ripetere la scansione.",
|
||||||
|
"scanningLocalNetworkOnPort": "Scansione della rete locale per dispositivi MeshCore WiFi sulla porta {port}",
|
||||||
|
"cancelScanAndAddServer": "Annulla la scansione e aggiungi un server",
|
||||||
|
"addServer": "Aggiungi server",
|
||||||
|
"scanningProgressIps": "Scansione... {scanned}/{total} IP",
|
||||||
|
"cancelAndEnterManually": "Annulla e inserisci manualmente",
|
||||||
|
"noRecentOrDiscoveredServers": "Nessun server recente o rilevato per ora",
|
||||||
|
"recentlyUsed": "Usati di recente",
|
||||||
|
"connecting": "Connessione...",
|
||||||
|
"discoveredOnThisNetwork": "Rilevati su questa rete",
|
||||||
|
"serverNoLongerAvailable": "Il server {host}:{port} non è più disponibile. Esegui di nuovo la scansione per trovare server attivi.",
|
||||||
|
"failedToConnectToHost": "Impossibile connettersi a {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Inserisci un indirizzo IP valido",
|
||||||
|
"enterValidTcpPort": "Inserisci una porta TCP valida",
|
||||||
|
"ipAddress": "Indirizzo IP",
|
||||||
|
"tcpPort": "Porta TCP",
|
||||||
|
"customServerPort": "Porta server personalizzata",
|
||||||
|
"chat": "Chat",
|
||||||
|
"restoringPreviousLink": "Ripristino del collegamento precedente",
|
||||||
|
"noDeviceConnected": "Nessun dispositivo connesso",
|
||||||
|
"sendAdvert": "Invia il mio contatto",
|
||||||
|
"advertMode": "Invia il mio contatto",
|
||||||
|
"advertModeSubtitle": "Scegli quanto lontano devono viaggiare le tue informazioni di contatto.",
|
||||||
|
"floodAdvertSent": "Contatto inviato attraverso la rete mesh",
|
||||||
|
"directAdvertSent": "Contatto inviato ai dispositivi vicini"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Button to restart BLE scanning"
|
"description": "Button to restart BLE scanning"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Dotknij, aby połączyć",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Subtitle text for device in scan list"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Urządzenie nie jest połączone",
|
"deviceNotConnected": "Urządzenie nie jest połączone",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Error message when device is not connected"
|
"description": "Error message when device is not connected"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Setting to show RX/TX indicators"
|
"description": "Setting to show RX/TX indicators"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Wyświetlaj wskaźniki aktywności pakietów na górnym pasku",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description for RX/TX indicators setting"
|
|
||||||
},
|
|
||||||
"disableMap": "Wyłącz mapę",
|
"disableMap": "Wyłącz mapę",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Setting to disable the map tab"
|
"description": "Setting to disable the map tab"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Ukryj kartę mapy, aby zmniejszyć zużycie baterii",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description for disable map setting"
|
|
||||||
},
|
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Language setting label"
|
"description": "Language setting label"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "App version label"
|
"description": "App version label"
|
||||||
},
|
},
|
||||||
"appName": "Nazwa aplikacji",
|
|
||||||
"@appName": {
|
|
||||||
"description": "App name label"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "O MeshCore SAR",
|
"aboutMeshCoreSar": "O MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "About dialog title"
|
"description": "About dialog title"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action to reset contact path for re-routing"
|
"description": "Action to reset contact path for re-routing"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Klucz publiczny skopiowano do schowka",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Success message when public key is copied"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "Skopiowano {label} do schowka",
|
"copiedToClipboard": "Skopiowano {label} do schowka",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Success message when a value is copied to clipboard",
|
"description": "Success message when a value is copied to clipboard",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Last seen label"
|
"description": "Last seen label"
|
||||||
},
|
},
|
||||||
"roomStatus": "Status pokoju",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Room status section header"
|
|
||||||
},
|
|
||||||
"loginStatus": "Status logowania",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Login status label"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Niezalogowany",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Not logged in status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Dostęp administratora",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Admin access label"
|
|
||||||
},
|
|
||||||
"yes": "Tak",
|
"yes": "Tak",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Yes answer"
|
"description": "Yes answer"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Permissions label"
|
"description": "Permissions label"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Hasło zapisano",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Password saved label"
|
|
||||||
},
|
|
||||||
"locationColon": "Lokalizacja:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Location section header"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Telemetry section header"
|
"description": "Telemetry section header"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS from telemetry label"
|
"description": "GPS from telemetry label"
|
||||||
},
|
},
|
||||||
"updated": "Zaktualizowano",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Updated timestamp label"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Trasa dla {name} została zresetowana. Następna wiadomość znajdzie nową trasę.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Info message after path reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Zaloguj ponownie do pokoju",
|
"reLoginToRoom": "Zaloguj ponownie do pokoju",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Button to re-login to room"
|
"description": "Button to re-login to room"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Label for radio frequency field"
|
"description": "Label for radio frequency field"
|
||||||
},
|
},
|
||||||
"frequencyExample": "np. 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Helper text example for frequency"
|
|
||||||
},
|
|
||||||
"bandwidth": "Szerokość pasma",
|
"bandwidth": "Szerokość pasma",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Label for bandwidth dropdown"
|
"description": "Label for bandwidth dropdown"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
"contactTrails": "Ślady kontaktów",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Contact trails section header"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Pokaż wszystkie ślady kontaktów",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Toggle label to show all contact trails"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Brak kontaktów z historią lokalizacji",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtitle when no contacts have trails"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Pokazywane są ślady dla {count} kontaktów",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtitle showing number of contacts with trails",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Indywidualne ślady kontaktów",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Expansion tile title for individual contact trails"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Informacje o urządzeniu",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Device information section header"
|
|
||||||
},
|
|
||||||
"bleName": "Nazwa BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Bluetooth Low Energy device name label"
|
|
||||||
},
|
|
||||||
"meshName": "Nazwa mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh network name label"
|
|
||||||
},
|
|
||||||
"notSet": "Nie ustawiono",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Label when a value is not set"
|
|
||||||
},
|
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Device model label"
|
"description": "Device model label"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Version label"
|
"description": "Version label"
|
||||||
},
|
},
|
||||||
"buildDate": "Data kompilacji",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Firmware build date label"
|
|
||||||
},
|
|
||||||
"firmware": "Firmware",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Firmware label"
|
|
||||||
},
|
|
||||||
"maxContacts": "Maks. kontaktów",
|
"maxContacts": "Maks. kontaktów",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Maximum contacts capacity label"
|
"description": "Maximum contacts capacity label"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Public information section header"
|
"description": "Public information section header"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Nazwa sieci mesh",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh network name field label"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Nazwa nadawana w ogłoszeniach mesh",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Helper text for mesh network name field"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetria i udostępnianie lokalizacji",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Telemetry and location sharing toggle label"
|
|
||||||
},
|
|
||||||
"lat": "Szer.",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Latitude field label (short form)"
|
|
||||||
},
|
|
||||||
"lon": "Dł.",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Longitude field label (short form)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Użyj bieżącej lokalizacji",
|
"useCurrentLocation": "Użyj bieżącej lokalizacji",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip for use current location button"
|
"description": "Tooltip for use current location button"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "Skopiowano {label} do schowka",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Short success message when copying to clipboard",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Nie udało się zapisać: {error}",
|
"failedToSave": "Nie udało się zapisać: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Generic error message for save failures",
|
"description": "Generic error message for save failures",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "SAR templates menu title"
|
"description": "SAR templates menu title"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "Zarządzaj szablonami SAR",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "Subtitle for SAR templates settings"
|
|
||||||
},
|
|
||||||
"addTemplate": "Dodaj szablon",
|
"addTemplate": "Dodaj szablon",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "Button to add new SAR template"
|
"description": "Button to add new SAR template"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "OK button label"
|
"description": "OK button label"
|
||||||
},
|
},
|
||||||
"permissionsSection": "Uprawnienia",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "Permissions section header"
|
|
||||||
},
|
|
||||||
"locationPermission": "Uprawnienie lokalizacji",
|
"locationPermission": "Uprawnienie lokalizacji",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "Location permission label"
|
"description": "Location permission label"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "Welcome wizard first page title"
|
"description": "Welcome wizard first page title"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "Potężne narzędzie komunikacji poza siecią do operacji poszukiwawczo-ratowniczych. Połącz się ze swoim zespołem przy użyciu technologii radia mesh, gdy tradycyjne sieci są niedostępne.",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "Welcome wizard first page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "Łączenie z radiem",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "Wizard connecting page title"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "Połącz smartfon z urządzeniem radiowym MeshCore przez Bluetooth, aby rozpocząć komunikację poza siecią.",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "Wizard connecting page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "Skanuj pobliskie urządzenia MeshCore",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "Wizard connecting feature 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "Sparuj radio przez Bluetooth",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "Wizard connecting feature 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "Działa całkowicie offline - internet nie jest wymagany",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "Wizard connecting feature 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "Kanały",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "Wizard channel page title"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "Nadawaj wiadomości do wszystkich na kanale, idealne do ogłoszeń i koordynacji całego zespołu.",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "Wizard channel page description"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "Kanał publiczny do ogólnej komunikacji zespołu",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "Wizard channel feature 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "Twórz własne kanały dla konkretnych grup",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "Wizard channel feature 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "Wiadomości są automatycznie przekazywane przez mesh",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "Wizard channel feature 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "Kontakty",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "Wizard contacts page title"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "Członkowie zespołu pojawiają się automatycznie, gdy dołączają do sieci mesh. Wyślij im wiadomość bezpośrednią lub zobacz ich lokalizację.",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "Wizard contacts page description"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "Kontakty wykrywane automatycznie",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "Wizard contacts feature 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "Wysyłaj prywatne wiadomości bezpośrednie",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "Wizard contacts feature 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "Podgląd poziomu baterii i czasu ostatniej aktywności",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "Wizard contacts feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "Mapa i lokalizacja",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "Wizard map page title"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "Śledź swój zespół w czasie rzeczywistym i oznaczaj ważne lokalizacje dla działań poszukiwawczo-ratowniczych.",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "Wizard map page description"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "Znaczniki SAR dla odnalezionych osób, pożarów i stref zbiórki",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "Wizard map feature 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "Śledzenie GPS członków zespołu w czasie rzeczywistym",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "Wizard map feature 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "Pobieraj mapy offline dla odległych obszarów",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "Wizard map feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "Rysuj kształty i udostępniaj informacje taktyczne",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "Wizard map feature 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "Pokaż samouczek powitalny",
|
"viewWelcomeTutorial": "Pokaż samouczek powitalny",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "Settings option to re-show welcome wizard"
|
"description": "Settings option to re-show welcome wizard"
|
||||||
@@ -2328,5 +2093,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Aktywny",
|
||||||
|
"addContact": "Dodaj kontakt",
|
||||||
|
"all": "Wszystko",
|
||||||
|
"clearAllLabel": "Wyczyść wszystko",
|
||||||
|
"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",
|
||||||
|
"trace": "Śledzenie",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"hideRepeatersOnMap": "Ukryj przekaźniki na mapie",
|
||||||
|
"setMapScale": "Ustaw skalę mapy",
|
||||||
|
"customMapScaleSaved": "Niestandardowa skala mapy zapisana",
|
||||||
|
"voiceBitrate": "Bitrate głosu",
|
||||||
|
"voiceCompressor": "Kompresor głosu",
|
||||||
|
"voiceLimiter": "Limiter głosu",
|
||||||
|
"micAutoGain": "Automatyczne wzmocnienie mikrofonu",
|
||||||
|
"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",
|
||||||
|
"busy": "Zajęty",
|
||||||
|
"searchMessages": "Szukaj wiadomości",
|
||||||
|
"sendImageFromGallery": "Wyślij obraz z galerii",
|
||||||
|
"takePhoto": "Zrób zdjęcie",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Automatyczne wysyłanie",
|
||||||
|
"autoSendDescription": "Wyślij bieżącą trasą.",
|
||||||
|
"sendDirect": "Wysyłanie bezpośrednie",
|
||||||
|
"sendDirectDescription": "Wyślij bezpośrednio do tego kontaktu.",
|
||||||
|
"sendFlood": "Wysyłanie zalewowe",
|
||||||
|
"sendFloodDescription": "Wyślij przez wszystkie przekaźniki.",
|
||||||
|
"postConnectDiscoveryTitle": "Odkryć przekaźniki teraz?",
|
||||||
|
"postConnectDiscoveryDescription": "Uruchom odkrywanie przekaźników zaraz po połączeniu, aby zobaczyć pobliskie węzły MeshCore i szybciej dodać je do sieci.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Dodaj",
|
||||||
|
"addAdditionalDetails": "Dodaj dodatkowe szczegóły",
|
||||||
|
"addPeer": "Dodaj węzeł",
|
||||||
|
"addPeerManually": "Dodaj węzeł ręcznie",
|
||||||
|
"addedToContacts": "{name} dodano do kontaktów",
|
||||||
|
"addedToSensors": "{name} dodano do czujników",
|
||||||
|
"allowAll": "Zezwól na wszystko",
|
||||||
|
"anonymousRxStats": "Anonimowe statystyki RX",
|
||||||
|
"appearance": "Wygląd",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Telemetria bazowa",
|
||||||
|
"blePin": "PIN BLE",
|
||||||
|
"buzzerAlerts": "Alerty brzęczyka",
|
||||||
|
"cancelImageReceive": "Anuluj odbieranie obrazu",
|
||||||
|
"choose": "Wybierz",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Wybierz, które zakładki i sekcje kontaktów mają być widoczne",
|
||||||
|
"clearCache": "Wyczyść pamięć podręczną",
|
||||||
|
"clearHistory": "Wyczyść historię",
|
||||||
|
"clearLiveView": "Wyczyść widok na żywo",
|
||||||
|
"clearLogs": "Wyczyść logi",
|
||||||
|
"clearOfflineCache": "Wyczyścić pamięć podręczną offline?",
|
||||||
|
"clearOverlay": "Wyczyść nakładkę",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Wyczyść trasę dopiero po wyczerpaniu wszystkich prób",
|
||||||
|
"codecBitrateAndAudioProcessing": "Kodek, bitrate i przetwarzanie dźwięku",
|
||||||
|
"connectByIpAddress": "Połącz przez adres IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Kontroluj, które alerty otrzymujesz",
|
||||||
|
"copyFullHex": "Kopiuj pełny Hex",
|
||||||
|
"copyRawResponse": "Kopiuj surową odpowiedź",
|
||||||
|
"couldNotLoadMetHistory": "Nie udało się wczytać historii MET",
|
||||||
|
"customizeSensor": "Dostosuj {name}",
|
||||||
|
"data": "Dane",
|
||||||
|
"deleteGroup": "Usuń grupę",
|
||||||
|
"deleteStyleConfirm": "Usunąć {name}?",
|
||||||
|
"deleteThisDrawing": "Usunąć ten {type}?",
|
||||||
|
"deny": "Odmów",
|
||||||
|
"deviceInfo": "Informacje o urządzeniu",
|
||||||
|
"deviceSettings": "Ustawienia urządzenia",
|
||||||
|
"deviceTimeSynced": "Czas urządzenia zsynchronizowany z tym telefonem.",
|
||||||
|
"disableFastGpsPublishing": "Wyłącz szybkie publikowanie GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Wyświetlanie, znaczniki i baza danych tras",
|
||||||
|
"download": "Pobierz",
|
||||||
|
"environmentalTelemetry": "Telemetria środowiskowa",
|
||||||
|
"errorCheckingForUpdates": "Błąd sprawdzania aktualizacji: {error}",
|
||||||
|
"errorGeneric": "Błąd: {error}",
|
||||||
|
"errorOpeningDownload": "Błąd otwierania pobranego pliku: {error}",
|
||||||
|
"exportLogs": "Eksportuj logi",
|
||||||
|
"extraAggressiveAvifCompression": "Bardzo agresywna kompresja AVIF",
|
||||||
|
"failedToAddContact": "Nie udało się dodać kontaktu: {error}",
|
||||||
|
"failedToAddName": "Nie udało się dodać {name}: {error}",
|
||||||
|
"failedToClearChannels": "Nie udało się wyczyścić kanałów: {error}",
|
||||||
|
"failedToClearContacts": "Nie udało się wyczyścić kontaktów: {error}",
|
||||||
|
"failedToConnectViaSerial": "Nie udało się połączyć przez port szeregowy",
|
||||||
|
"failedToLoadPreviewImage": "Nie udało się wczytać podglądu obrazu: {error}",
|
||||||
|
"failedToLoadTrace": "Nie udało się wczytać trasy: {error}",
|
||||||
|
"failedToSendSarMarker": "Nie udało się wysłać znacznika SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Nie udało się ustawić trybu brzęczyka: {error}",
|
||||||
|
"failedToSetGpsMode": "Nie udało się ustawić trybu GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Nie udało się zsynchronizować czasu urządzenia: {error}",
|
||||||
|
"failedToWipeDeviceData": "Nie udało się wymazać danych urządzenia: {error}",
|
||||||
|
"fastGpsTargetChannel": "Docelowy kanał szybkiego GPS",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Szybkie aktualizacje, progi i uprawnienia",
|
||||||
|
"fetchingCatalog": "Pobieranie katalogu...",
|
||||||
|
"fifteenMinutes": "15 minut",
|
||||||
|
"fifteenSeconds": "15 sekund",
|
||||||
|
"filterByDirection": "Filtruj według kierunku",
|
||||||
|
"finish": "Zakończ",
|
||||||
|
"fiveMinutes": "5 minut",
|
||||||
|
"fiveSeconds": "5 sekund",
|
||||||
|
"fix": "Napraw",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Zasady rozgłaszania GPS",
|
||||||
|
"gpsAndLocation": "GPS i lokalizacja",
|
||||||
|
"gpsDiagnostics": "Diagnostyka GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Pomoc",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Ukryte",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Jak radio automatycznie dodaje odkryte węzły.",
|
||||||
|
"images": "Obrazy",
|
||||||
|
"importProfile": "Importuj profil",
|
||||||
|
"lastFix": "Ostatni fix",
|
||||||
|
"latest": "Najnowsze",
|
||||||
|
"latitude": "Szerokość geograficzna",
|
||||||
|
"loadASavedRegion": "Wczytaj zapisany region",
|
||||||
|
"loading": "Ładowanie",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Udostępnianie lokalizacji, sprzęt i interwał aktualizacji.",
|
||||||
|
"locationTelemetry": "Telemetria lokalizacji",
|
||||||
|
"lockDestination": "Zablokuj cel",
|
||||||
|
"longitude": "Długość geograficzna",
|
||||||
|
"mapStyle": "Styl mapy",
|
||||||
|
"max": "Maks.",
|
||||||
|
"maxZoom": "Maks. przybliżenie",
|
||||||
|
"messaging": "Wiadomości",
|
||||||
|
"metHistory": "Historia MET",
|
||||||
|
"min": "Min.",
|
||||||
|
"minZoom": "Min. przybliżenie",
|
||||||
|
"moveDown": "Przesuń w dół",
|
||||||
|
"moveUp": "Przesuń w górę",
|
||||||
|
"multiAckMode": "Tryb multi-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Zarządzanie przestrzenią roboczą wielu urządzeń",
|
||||||
|
"myLocation": "Moja lokalizacja",
|
||||||
|
"nameAndTelemetryShared": "Nazwa i telemetria udostępniana innym urządzeniom.",
|
||||||
|
"newVersionAvailable": "Nowa wersja: {version}",
|
||||||
|
"newer": "Nowsze",
|
||||||
|
"noNeighboursFound": "Nie znaleziono sąsiadów",
|
||||||
|
"noServersFound": "Nie znaleziono serwerów",
|
||||||
|
"notSet": "Nie ustawiono",
|
||||||
|
"notifications": "Powiadomienia",
|
||||||
|
"offZeroSeconds": "Wył. (0 s)",
|
||||||
|
"offlineMaps": "Mapy offline",
|
||||||
|
"older": "Starsze",
|
||||||
|
"oneByteMode0": "1 bajt (tryb 0)",
|
||||||
|
"oneHour": "1 godzina",
|
||||||
|
"oneMinute": "1 minuta",
|
||||||
|
"openPacketLogs": "Otwórz logi pakietów",
|
||||||
|
"packetTypeHelp": "Pomoc typów pakietów",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Wklej link udostępniania lub ogłoszenie Hex",
|
||||||
|
"pathHash": "Hash ścieżki",
|
||||||
|
"pathSize": "Rozmiar ścieżki",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Wielokąt",
|
||||||
|
"rawResponseCopied": "Skopiowano surową odpowiedź",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Ponów wysyłkę przez najbliższy przekaźnik po niepowodzeniu",
|
||||||
|
"resolveContact": "Rozwiąż kontakt",
|
||||||
|
"routingRetriesAndDestinationLock": "Routing, ponawianie i blokada celu",
|
||||||
|
"samples": "Próbki",
|
||||||
|
"satellites": "Satelity",
|
||||||
|
"saving": "Zapisywanie...",
|
||||||
|
"search": "Szukaj",
|
||||||
|
"searchByNameOrToken": "Szukaj po nazwie lub tokenie",
|
||||||
|
"searchDiscoveredNodes": "Szukaj odkrytych węzłów",
|
||||||
|
"searchInCurrentFilter": "Szukaj w bieżącym filtrze",
|
||||||
|
"searchLogs": "Szukaj w logach...",
|
||||||
|
"searchSensors": "Szukaj czujników",
|
||||||
|
"searchThisSection": "Szukaj w tej sekcji",
|
||||||
|
"selectedRelays": "Wybrane przekaźniki",
|
||||||
|
"sensor": "Czujnik",
|
||||||
|
"serialError": "Błąd portu szeregowego: {error}",
|
||||||
|
"setPathForContact": "Ustaw ścieżkę dla {name}",
|
||||||
|
"shareLiveGps": "Udostępnij GPS na żywo",
|
||||||
|
"shareMyTiles": "Udostępnij moje kafelki",
|
||||||
|
"sharingLocationFromPhone": "Udostępnianie lokalizacji na tym kanale z telefonu.",
|
||||||
|
"sharingLocationFromRadio": "Udostępnianie lokalizacji na tym kanale z radia.",
|
||||||
|
"signalDbm": "Sygnał {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Rozmiar, kompresja i podgląd",
|
||||||
|
"skip": "Pomiń",
|
||||||
|
"smallerFileSize": "Mniejszy rozmiar pliku",
|
||||||
|
"sort": "Sortuj",
|
||||||
|
"stoppedSharingLocation": "Zatrzymano udostępnianie lokalizacji na tym kanale.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Wycisz powiadomienia, gdy aplikacja jest na pierwszym planie",
|
||||||
|
"tabsAndNavigation": "Zakładki i nawigacja",
|
||||||
|
"templatesAndTutorials": "Szablony i samouczki",
|
||||||
|
"tenMinutes": "10 minut",
|
||||||
|
"tenSeconds": "10 sekund",
|
||||||
|
"testSendUpdate": "Testowe wysłanie aktualizacji",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Motyw, język i preferencje wyświetlania",
|
||||||
|
"thirtyMinutes": "30 minut",
|
||||||
|
"thirtySeconds": "30 sekund",
|
||||||
|
"threeBytesMode2": "3 bajty (tryb 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Statystyki ruchu, historia wiadomości i narzędzia deweloperskie",
|
||||||
|
"twoBytesMode1": "2 bajty (tryb 1)",
|
||||||
|
"twoMinutes": "2 minuty",
|
||||||
|
"undo": "Cofnij",
|
||||||
|
"updated": "Zaktualizowano",
|
||||||
|
"useContactFlags": "Używaj flag kontaktów",
|
||||||
|
"useSavedCoordinates": "Użyj zapisanych współrzędnych",
|
||||||
|
"view": "Widok",
|
||||||
|
"viewNeighbours": "Pokaż sąsiadów",
|
||||||
|
"viewPublicStats": "Pokaż publiczne statystyki",
|
||||||
|
"setRegionScope": "Ustaw zakres regionu",
|
||||||
|
"regionScope": "Zakres regionu",
|
||||||
|
"regionScopeNone": "Brak (globalny)",
|
||||||
|
"clearRegionScope": "Wyczyść zakres",
|
||||||
|
"regionScopeWarning": "Tylko przekaźniki zezwalające na ten region będą przekazywać.",
|
||||||
|
"discoverRegions": "Odkryj z przekaźników",
|
||||||
|
"discoveringRegions": "Odkrywanie regionów...",
|
||||||
|
"enterRegionName": "Nazwa regionu (np. auckland)",
|
||||||
|
"noRegionsFound": "Nie znaleziono regionów na tym przekaźniku.",
|
||||||
|
"regionScopeSet": "Zakres regionu ustawiony na {name}",
|
||||||
|
"regionScopeCleared": "Zakres regionu wyczyszczony",
|
||||||
|
"simpleMode": "Tryb prosty",
|
||||||
|
"simpleModeDescription": "Tylko czat i mapa z ulubionymi kontaktami",
|
||||||
|
"simpleModeRequiresLockedDestination": "Aby użyć trybu prostego, najpierw włącz „Zablokuj cel” z kanałem",
|
||||||
|
"noFavouriteContacts": "Brak ulubionych kontaktów",
|
||||||
|
"selectDestinationFirst": "Najpierw wybierz kanał, kontakt lub pokój",
|
||||||
|
"recipientNoLongerAvailable": "Odbiorca nie jest już dostępny",
|
||||||
|
"failedToSendMessage": "Nie udało się wysłać: {error}",
|
||||||
|
"tictactoeDirectOnly": "Kółko i krzyżyk działa tylko w wiadomościach bezpośrednich. Najpierw wybierz kontakt.",
|
||||||
|
"deviceKeyUnavailable": "Klucz urządzenia jest niedostępny",
|
||||||
|
"imageCompressionFailed": "Kompresja obrazu nie powiodła się",
|
||||||
|
"imageFragmentationFailed": "Fragmentacja obrazu nie powiodła się",
|
||||||
|
"failedToAnnounceImage": "Nie udało się ogłosić obrazu",
|
||||||
|
"noRecipientSelected": "Nie wybrano odbiorcy",
|
||||||
|
"imageSendFailed": "Wysyłanie obrazu nie powiodło się",
|
||||||
|
"microphonePermissionRequiredForVoice": "Wiadomości głosowe wymagają uprawnienia do mikrofonu",
|
||||||
|
"recordingDiscarded": "Nagranie odrzucone",
|
||||||
|
"publicChannelMediaWarning": "Za chwilę wyślesz {mediaType} na kanał publiczny. Nie jest to zalecane, ponieważ może to odebrać każdy w sieci mesh. Wybierz kanał prywatny lub oznaczony, chyba że właśnie tego chcesz.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Wybierz prywatny kanał, aby udostępnić swoją lokalizację.",
|
||||||
|
"stopSharingMyLocation": "Przestań udostępniać moją lokalizację",
|
||||||
|
"shareMyLocation": "Udostępnij moją lokalizację",
|
||||||
|
"stopRecording": "Zatrzymaj nagrywanie",
|
||||||
|
"recordVoice": "Nagraj wiadomość głosową",
|
||||||
|
"moreActions": "Więcej działań",
|
||||||
|
"noMessagesInFilter": "Brak wiadomości w tym filtrze",
|
||||||
|
"noMatchesInFilter": "Brak dopasowań w tym filtrze",
|
||||||
|
"syncFailed": "Synchronizacja nie powiodła się: {error}",
|
||||||
|
"invalidLink": "Nieprawidłowy link",
|
||||||
|
"cannotOpenLink": "Nie można otworzyć linku",
|
||||||
|
"failedToOpenLink": "Nie udało się otworzyć linku",
|
||||||
|
"failedToResendMessage": "Nie udało się ponownie wysłać wiadomości",
|
||||||
|
"retryFailed": "Ponowna próba nie powiodła się: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Usuń rysunek i wiadomość",
|
||||||
|
"lastEchoRelay": "Ostatnie echo – przekaźnik",
|
||||||
|
"lastEchoPath": "Ostatnie echo – ścieżka",
|
||||||
|
"lastEchoBytesReport": "Ostatnie echo – raport bajtów",
|
||||||
|
"noGpsCoordinatesAvailable": "Brak dostępnych współrzędnych GPS",
|
||||||
|
"notASarMarker": "To nie jest znacznik SAR",
|
||||||
|
"failedToSaveTemplate": "Nie udało się zapisać szablonu: {error}",
|
||||||
|
"drawingNotFound": "Nie znaleziono rysunku",
|
||||||
|
"unknownDrawingType": "Nieznany typ rysunku",
|
||||||
|
"customMapMarker": "Niestandardowy znacznik mapy",
|
||||||
|
"customMapPointLabel": "Punkt: {point}",
|
||||||
|
"mapIdLabel": "ID mapy: {id}",
|
||||||
|
"cannotFetchVoice": "Nie można pobrać wiadomości głosowej",
|
||||||
|
"senderContactUnknown": "Kontakt nadawcy jest nieznany. Najpierw zsynchronizuj kontakty.",
|
||||||
|
"senderRouteUnknown": "Trasa nadawcy jest nieznana. Najpierw zsynchronizuj kontakty/ścieżkę.",
|
||||||
|
"messageTooFar": "Wiadomość jest zbyt daleko ({hops} skoków, maks. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Trasa nadawcy nie odpowiedziała na sprawdzenie ścieżki. Zsynchronizuj kontakty/ścieżkę i spróbuj ponownie.",
|
||||||
|
"senderRouteNoRawResponse": "Trasa nadawcy nie odpowiedziała na surowej ścieżce transportowej.",
|
||||||
|
"voiceFetchOverHops": "Pobieranie głosu przez {hops} skoków może chwilę potrwać.",
|
||||||
|
"voiceReceiveCanceled": "Odbieranie głosu anulowane",
|
||||||
|
"fetchingMissingVoiceFragments": "Pobieranie brakujących fragmentów głosu",
|
||||||
|
"receivingVoice": "Odbieranie głosu",
|
||||||
|
"cannotFetchImage": "Nie można pobrać obrazu",
|
||||||
|
"imageFetchOverHops": "Pobieranie obrazu przez {hops} skoków może chwilę potrwać.",
|
||||||
|
"imageAlreadyBeingReceived": "Obraz jest już odbierany",
|
||||||
|
"loadImage": "Wczytaj obraz",
|
||||||
|
"imageFetchFailedToSendRequest": "Nie udało się wysłać żądania pobrania obrazu",
|
||||||
|
"imageUnavailable": "Obraz chwilowo niedostępny",
|
||||||
|
"imageFetchTimedOut": "Upłynął limit czasu pobierania obrazu",
|
||||||
|
"imageReceiveCanceled": "Odbieranie obrazu anulowane",
|
||||||
|
"closeImagePreview": "Zamknij podgląd obrazu",
|
||||||
|
"recordingReleaseToSend": "Nagrywanie... puść, aby wysłać",
|
||||||
|
"sendingVoice": "Wysyłanie głosu...",
|
||||||
|
"sendLongPressToRecordVoice": "Wyślij (przytrzymaj, aby nagrać głos)",
|
||||||
|
"discardRecording": "Odrzuć nagranie",
|
||||||
|
"hopCount": "{count, plural, one{{count} skok} few{{count} skoki} many{{count} skoków} other{{count} skoku}}",
|
||||||
|
"channelsSectionDescription": "Kanały rozgłoszeniowe dla pobliskiego ruchu mesh",
|
||||||
|
"roomsSectionDescription": "Wspólne przestrzenie do bieżącej koordynacji zespołu",
|
||||||
|
"contactsSectionDescription": "Osoby i urządzenia w bezpośrednim zasięgu",
|
||||||
|
"noRecentChatters": "Brak ostatnich rozmówców",
|
||||||
|
"public": "Publiczny",
|
||||||
|
"failedToSendTicTacToeMove": "Nie udało się wysłać ruchu w kółko i krzyżyk",
|
||||||
|
"youWon": "Wygrałeś",
|
||||||
|
"opponentWon": "Wygrał przeciwnik",
|
||||||
|
"gameDraw": "Remis",
|
||||||
|
"yourTurn": "Twoja kolej",
|
||||||
|
"opponentTurn": "Kolej przeciwnika",
|
||||||
|
"pleaseEnterValidCoordinates": "Wprowadź prawidłowe współrzędne",
|
||||||
|
"locationNotAvailable": "Lokalizacja niedostępna",
|
||||||
|
"customCaveMapPoint": "Niestandardowy punkt mapy jaskini",
|
||||||
|
"mapPoint": "Punkt mapy",
|
||||||
|
"fetchingMissingImageFragments": "Pobieranie brakujących fragmentów…",
|
||||||
|
"loadingImage": "Wczytywanie…",
|
||||||
|
"receivingImage": "Odbieranie…",
|
||||||
|
"tapToLoad": "Dotknij, aby wczytać",
|
||||||
|
"connectDevice": "Połącz urządzenie",
|
||||||
|
"chooseTransportSubtitle": "Wybierz transport: Bluetooth, WiFi lub port szeregowy",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Sieć",
|
||||||
|
"serial": "Port szeregowy",
|
||||||
|
"scan": "Skanuj",
|
||||||
|
"pressScanToSearchForDevices": "Naciśnij Skanuj, aby wyszukać pobliskie urządzenia",
|
||||||
|
"failedToConnectToDevice": "Nie udało się połączyć z {name}",
|
||||||
|
"unknownDevice": "Nieznane urządzenie",
|
||||||
|
"showingCachedResultsTapRefresh": "Wyświetlane są wyniki z pamięci podręcznej. Dotknij odświeżania, aby zeskanować ponownie.",
|
||||||
|
"scanningLocalNetworkOnPort": "Skanowanie sieci lokalnej w poszukiwaniu urządzeń MeshCore WiFi na porcie {port}",
|
||||||
|
"cancelScanAndAddServer": "Anuluj skanowanie i dodaj serwer",
|
||||||
|
"addServer": "Dodaj serwer",
|
||||||
|
"scanningProgressIps": "Skanowanie... {scanned}/{total} adresów IP",
|
||||||
|
"cancelAndEnterManually": "Anuluj i wprowadź ręcznie",
|
||||||
|
"noRecentOrDiscoveredServers": "Brak ostatnich ani wykrytych serwerów",
|
||||||
|
"recentlyUsed": "Ostatnio używane",
|
||||||
|
"connecting": "Łączenie...",
|
||||||
|
"discoveredOnThisNetwork": "Wykryte w tej sieci",
|
||||||
|
"serverNoLongerAvailable": "Serwer {host}:{port} nie jest już dostępny. Zeskanuj ponownie, aby znaleźć aktywne serwery.",
|
||||||
|
"failedToConnectToHost": "Nie udało się połączyć z {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Wprowadź prawidłowy adres IP",
|
||||||
|
"enterValidTcpPort": "Wprowadź prawidłowy port TCP",
|
||||||
|
"ipAddress": "Adres IP",
|
||||||
|
"tcpPort": "Port TCP",
|
||||||
|
"customServerPort": "Niestandardowy port serwera",
|
||||||
|
"chat": "Czat",
|
||||||
|
"restoringPreviousLink": "Przywracanie poprzedniego połączenia",
|
||||||
|
"noDeviceConnected": "Brak połączonego urządzenia",
|
||||||
|
"sendAdvert": "Wyślij mój kontakt",
|
||||||
|
"advertMode": "Wyślij mój kontakt",
|
||||||
|
"advertModeSubtitle": "Wybierz, jak daleko mają dotrzeć Twoje dane kontaktowe.",
|
||||||
|
"floodAdvertSent": "Kontakt wysłano do całej sieci mesh",
|
||||||
|
"directAdvertSent": "Kontakt wysłano do urządzeń w pobliżu"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Button to restart BLE scanning"
|
"description": "Button to restart BLE scanning"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Toque para conectar",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Subtitle text for device in scan list"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Dispositivo não conectado",
|
"deviceNotConnected": "Dispositivo não conectado",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Error message when device is not connected"
|
"description": "Error message when device is not connected"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Setting to show RX/TX indicators"
|
"description": "Setting to show RX/TX indicators"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Exibir indicadores de atividade de pacotes na barra superior",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description for RX/TX indicators setting"
|
|
||||||
},
|
|
||||||
"disableMap": "Desativar mapa",
|
"disableMap": "Desativar mapa",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Setting to disable the map tab"
|
"description": "Setting to disable the map tab"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Ocultar a aba de mapa para reduzir o uso de bateria",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description for disable map setting"
|
|
||||||
},
|
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Language setting label"
|
"description": "Language setting label"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "App version label"
|
"description": "App version label"
|
||||||
},
|
},
|
||||||
"appName": "Nome do app",
|
|
||||||
"@appName": {
|
|
||||||
"description": "App name label"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Sobre o MeshCore SAR",
|
"aboutMeshCoreSar": "Sobre o MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "About dialog title"
|
"description": "About dialog title"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action to reset contact path for re-routing"
|
"description": "Action to reset contact path for re-routing"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Chave pública copiada para a área de transferência",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Success message when public key is copied"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} copiado para a área de transferência",
|
"copiedToClipboard": "{label} copiado para a área de transferência",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Success message when a value is copied to clipboard",
|
"description": "Success message when a value is copied to clipboard",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Last seen label"
|
"description": "Last seen label"
|
||||||
},
|
},
|
||||||
"roomStatus": "Status da sala",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Room status section header"
|
|
||||||
},
|
|
||||||
"loginStatus": "Status do login",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Login status label"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Não conectado",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Not logged in status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Acesso de administrador",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Admin access label"
|
|
||||||
},
|
|
||||||
"yes": "Sim",
|
"yes": "Sim",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Yes answer"
|
"description": "Yes answer"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Permissions label"
|
"description": "Permissions label"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Senha salva",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Password saved label"
|
|
||||||
},
|
|
||||||
"locationColon": "Localização:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Location section header"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetria",
|
"telemetry": "Telemetria",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Telemetry section header"
|
"description": "Telemetry section header"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS from telemetry label"
|
"description": "GPS from telemetry label"
|
||||||
},
|
},
|
||||||
"updated": "Atualizado",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Updated timestamp label"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Rota redefinida para {name}. A próxima mensagem encontrará um novo caminho.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Info message after path reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Entrar novamente na sala",
|
"reLoginToRoom": "Entrar novamente na sala",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Button to re-login to room"
|
"description": "Button to re-login to room"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Label for radio frequency field"
|
"description": "Label for radio frequency field"
|
||||||
},
|
},
|
||||||
"frequencyExample": "ex.: 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Helper text example for frequency"
|
|
||||||
},
|
|
||||||
"bandwidth": "Largura de banda",
|
"bandwidth": "Largura de banda",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Label for bandwidth dropdown"
|
"description": "Label for bandwidth dropdown"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
"contactTrails": "Trilhas dos contatos",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Contact trails section header"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Mostrar todas as trilhas dos contatos",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Toggle label to show all contact trails"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Nenhum contato com histórico de localização",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtitle when no contacts have trails"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Mostrando trilhas de {count} contatos",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtitle showing number of contacts with trails",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Trilhas individuais dos contatos",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Expansion tile title for individual contact trails"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Informações do dispositivo",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Device information section header"
|
|
||||||
},
|
|
||||||
"bleName": "Nome BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Bluetooth Low Energy device name label"
|
|
||||||
},
|
|
||||||
"meshName": "Nome da mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh network name label"
|
|
||||||
},
|
|
||||||
"notSet": "Não definido",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Label when a value is not set"
|
|
||||||
},
|
|
||||||
"model": "Modelo",
|
"model": "Modelo",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Device model label"
|
"description": "Device model label"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Version label"
|
"description": "Version label"
|
||||||
},
|
},
|
||||||
"buildDate": "Data de build",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Firmware build date label"
|
|
||||||
},
|
|
||||||
"firmware": "Firmware",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Firmware label"
|
|
||||||
},
|
|
||||||
"maxContacts": "Máx. de contatos",
|
"maxContacts": "Máx. de contatos",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Maximum contacts capacity label"
|
"description": "Maximum contacts capacity label"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Public information section header"
|
"description": "Public information section header"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Nome da rede mesh",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh network name field label"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Nome transmitido nos anúncios mesh",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Helper text for mesh network name field"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetria e compartilhamento de localização",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Telemetry and location sharing toggle label"
|
|
||||||
},
|
|
||||||
"lat": "Lat.",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Latitude field label (short form)"
|
|
||||||
},
|
|
||||||
"lon": "Lon.",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Longitude field label (short form)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Usar localização atual",
|
"useCurrentLocation": "Usar localização atual",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip for use current location button"
|
"description": "Tooltip for use current location button"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} copiado para a área de transferência",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Short success message when copying to clipboard",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Falha ao salvar: {error}",
|
"failedToSave": "Falha ao salvar: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Generic error message for save failures",
|
"description": "Generic error message for save failures",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "SAR templates menu title"
|
"description": "SAR templates menu title"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "Gerenciar modelos SAR",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "Subtitle for SAR templates settings"
|
|
||||||
},
|
|
||||||
"addTemplate": "Adicionar modelo",
|
"addTemplate": "Adicionar modelo",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "Button to add new SAR template"
|
"description": "Button to add new SAR template"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "OK button label"
|
"description": "OK button label"
|
||||||
},
|
},
|
||||||
"permissionsSection": "Permissões",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "Permissions section header"
|
|
||||||
},
|
|
||||||
"locationPermission": "Permissão de localização",
|
"locationPermission": "Permissão de localização",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "Location permission label"
|
"description": "Location permission label"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "Welcome wizard first page title"
|
"description": "Welcome wizard first page title"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "Uma poderosa ferramenta de comunicação off-grid para operações de busca e resgate. Conecte-se com sua equipe usando tecnologia de rádio mesh quando as redes tradicionais não estiverem disponíveis.",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "Welcome wizard first page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "Conectando ao seu rádio",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "Wizard connecting page title"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "Conecte seu smartphone a um dispositivo de rádio MeshCore via Bluetooth para começar a se comunicar off-grid.",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "Wizard connecting page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "Procure dispositivos MeshCore próximos",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "Wizard connecting feature 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "Emparelhe com seu rádio via Bluetooth",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "Wizard connecting feature 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "Funciona totalmente offline - internet não é necessária",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "Wizard connecting feature 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "Canais",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "Wizard channel page title"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "Transmita mensagens para todos em um canal, ideal para anúncios e coordenação de toda a equipe.",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "Wizard channel page description"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "Canal público para comunicação geral da equipe",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "Wizard channel feature 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "Crie canais personalizados para grupos específicos",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "Wizard channel feature 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "As mensagens são retransmitidas automaticamente pela malha",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "Wizard channel feature 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "Contatos",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "Wizard contacts page title"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "Os membros da sua equipe aparecem automaticamente à medida que entram na rede mesh. Envie mensagens diretas ou veja a localização deles.",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "Wizard contacts page description"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "Contatos descobertos automaticamente",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "Wizard contacts feature 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "Envie mensagens diretas privadas",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "Wizard contacts feature 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "Veja o nível de bateria e o horário da última visualização",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "Wizard contacts feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "Mapa e localização",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "Wizard map page title"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "Acompanhe sua equipe em tempo real e marque locais importantes para operações de busca e resgate.",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "Wizard map page description"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "Marcadores SAR para pessoas encontradas, incêndios e áreas de apoio",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "Wizard map feature 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "Rastreamento GPS em tempo real dos membros da equipe",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "Wizard map feature 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "Baixe mapas offline para áreas remotas",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "Wizard map feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "Desenhe formas e compartilhe informações táticas",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "Wizard map feature 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "Ver tutorial de boas-vindas",
|
"viewWelcomeTutorial": "Ver tutorial de boas-vindas",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "Settings option to re-show welcome wizard"
|
"description": "Settings option to re-show welcome wizard"
|
||||||
@@ -2328,5 +2093,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Ativo",
|
||||||
|
"addContact": "Adicionar contacto",
|
||||||
|
"all": "Tudo",
|
||||||
|
"clearAllLabel": "Limpar tudo",
|
||||||
|
"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",
|
||||||
|
"trace": "Rastreio",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"voiceLimiter": "Limitador de voz",
|
||||||
|
"micAutoGain": "Ganho automático do microfone",
|
||||||
|
"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",
|
||||||
|
"busy": "Ocupado",
|
||||||
|
"searchMessages": "Pesquisar mensagens",
|
||||||
|
"sendImageFromGallery": "Enviar imagem da galeria",
|
||||||
|
"takePhoto": "Tirar foto",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Envio automático",
|
||||||
|
"autoSendDescription": "Enviar pela rota atual.",
|
||||||
|
"sendDirect": "Envio direto",
|
||||||
|
"sendDirectDescription": "Enviar diretamente a este contacto.",
|
||||||
|
"sendFlood": "Envio por inundação",
|
||||||
|
"sendFloodDescription": "Enviar por todos os repetidores.",
|
||||||
|
"postConnectDiscoveryTitle": "Descobrir repetidores agora?",
|
||||||
|
"postConnectDiscoveryDescription": "Execute a descoberta de repetidores logo após a conexão para ver os nós MeshCore próximos e adicioná-los à sua rede mais rapidamente.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Adicionar",
|
||||||
|
"addAdditionalDetails": "Adicionar detalhes adicionais",
|
||||||
|
"addPeer": "Adicionar par",
|
||||||
|
"addPeerManually": "Adicionar par manualmente",
|
||||||
|
"addedToContacts": "{name} adicionado aos contatos",
|
||||||
|
"addedToSensors": "{name} adicionado aos Sensores",
|
||||||
|
"allowAll": "Permitir todos",
|
||||||
|
"anonymousRxStats": "Estatísticas RX anónimas",
|
||||||
|
"appearance": "Aparência",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Telemetria base",
|
||||||
|
"blePin": "PIN BLE",
|
||||||
|
"buzzerAlerts": "Alertas sonoros",
|
||||||
|
"cancelImageReceive": "Cancelar receção de imagem",
|
||||||
|
"choose": "Escolher",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Escolher quais separadores e secções de contatos mostrar",
|
||||||
|
"clearCache": "Limpar cache",
|
||||||
|
"clearHistory": "Limpar histórico",
|
||||||
|
"clearLiveView": "Limpar vista ao vivo",
|
||||||
|
"clearLogs": "Limpar registos",
|
||||||
|
"clearOfflineCache": "Limpar cache offline?",
|
||||||
|
"clearOverlay": "Limpar sobreposição",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Limpar rota apenas após todas as tentativas falharem",
|
||||||
|
"codecBitrateAndAudioProcessing": "Codec, bitrate e processamento de áudio",
|
||||||
|
"connectByIpAddress": "Conectar por endereço IP",
|
||||||
|
"controlWhichAlertsYouReceive": "Controlar quais alertas recebe",
|
||||||
|
"copyFullHex": "Copiar hex completo",
|
||||||
|
"copyRawResponse": "Copiar resposta bruta",
|
||||||
|
"couldNotLoadMetHistory": "Não foi possível carregar o histórico MET",
|
||||||
|
"customizeSensor": "Personalizar {name}",
|
||||||
|
"data": "Dados",
|
||||||
|
"deleteGroup": "Eliminar grupo",
|
||||||
|
"deleteStyleConfirm": "Eliminar {name}?",
|
||||||
|
"deleteThisDrawing": "Eliminar este {type}?",
|
||||||
|
"deny": "Negar",
|
||||||
|
"deviceInfo": "Informações do dispositivo",
|
||||||
|
"deviceSettings": "Definições do dispositivo",
|
||||||
|
"deviceTimeSynced": "Hora do dispositivo sincronizada com este telemóvel.",
|
||||||
|
"disableFastGpsPublishing": "Desativar publicação rápida de GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Ecrã, marcadores e base de dados de percursos",
|
||||||
|
"download": "Transferir",
|
||||||
|
"environmentalTelemetry": "Telemetria ambiental",
|
||||||
|
"errorCheckingForUpdates": "Erro ao verificar atualizações: {error}",
|
||||||
|
"errorGeneric": "Erro: {error}",
|
||||||
|
"errorOpeningDownload": "Erro ao abrir transferência: {error}",
|
||||||
|
"exportLogs": "Exportar registos",
|
||||||
|
"extraAggressiveAvifCompression": "Compressão AVIF extra-agressiva",
|
||||||
|
"failedToAddContact": "Falha ao adicionar contato: {error}",
|
||||||
|
"failedToAddName": "Falha ao adicionar {name}: {error}",
|
||||||
|
"failedToClearChannels": "Falha ao limpar canais: {error}",
|
||||||
|
"failedToClearContacts": "Falha ao limpar contatos: {error}",
|
||||||
|
"failedToConnectViaSerial": "Falha ao conectar via série",
|
||||||
|
"failedToLoadPreviewImage": "Falha ao carregar imagem de pré-visualização: {error}",
|
||||||
|
"failedToLoadTrace": "Falha ao carregar percurso: {error}",
|
||||||
|
"failedToSendSarMarker": "Falha ao enviar marcador SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Falha ao definir modo de alarme: {error}",
|
||||||
|
"failedToSetGpsMode": "Falha ao definir modo GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Falha ao sincronizar hora do dispositivo: {error}",
|
||||||
|
"failedToWipeDeviceData": "Falha ao apagar dados do dispositivo: {error}",
|
||||||
|
"fastGpsTargetChannel": "Canal alvo de GPS rápido",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Atualizações rápidas, limiares e permissões",
|
||||||
|
"fetchingCatalog": "A obter catálogo...",
|
||||||
|
"fifteenMinutes": "15 minutos",
|
||||||
|
"fifteenSeconds": "15 segundos",
|
||||||
|
"filterByDirection": "Filtrar por direção",
|
||||||
|
"finish": "Concluir",
|
||||||
|
"fiveMinutes": "5 minutos",
|
||||||
|
"fiveSeconds": "5 segundos",
|
||||||
|
"fix": "Correção",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Política de anúncio GPS",
|
||||||
|
"gpsAndLocation": "GPS e Localização",
|
||||||
|
"gpsDiagnostics": "Diagnósticos GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Ajuda",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Oculto",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Como o rádio adiciona automaticamente nós descobertos.",
|
||||||
|
"images": "Imagens",
|
||||||
|
"importProfile": "Importar perfil",
|
||||||
|
"lastFix": "Última correção",
|
||||||
|
"latest": "Mais recente",
|
||||||
|
"latitude": "Latitude",
|
||||||
|
"loadASavedRegion": "Carregar uma região guardada",
|
||||||
|
"loading": "A carregar",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Partilha de localização, hardware e intervalo de atualização.",
|
||||||
|
"locationTelemetry": "Telemetria de localização",
|
||||||
|
"lockDestination": "Bloquear destino",
|
||||||
|
"longitude": "Longitude",
|
||||||
|
"mapStyle": "Estilo do mapa",
|
||||||
|
"max": "Máx",
|
||||||
|
"maxZoom": "Zoom máximo",
|
||||||
|
"messaging": "Mensagens",
|
||||||
|
"metHistory": "Histórico MET",
|
||||||
|
"min": "Mín",
|
||||||
|
"minZoom": "Zoom mínimo",
|
||||||
|
"moveDown": "Mover para baixo",
|
||||||
|
"moveUp": "Mover para cima",
|
||||||
|
"multiAckMode": "Modo multi-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Gestão de espaço de trabalho multi-dispositivo",
|
||||||
|
"myLocation": "A minha localização",
|
||||||
|
"nameAndTelemetryShared": "Nome e telemetria partilhados com outros dispositivos.",
|
||||||
|
"newVersionAvailable": "Nova versão: {version}",
|
||||||
|
"newer": "Mais recente",
|
||||||
|
"noNeighboursFound": "Nenhum vizinho encontrado",
|
||||||
|
"noServersFound": "Nenhum servidor encontrado",
|
||||||
|
"notSet": "Não definido",
|
||||||
|
"notifications": "Notificações",
|
||||||
|
"offZeroSeconds": "Desligado (0 s)",
|
||||||
|
"offlineMaps": "Mapas offline",
|
||||||
|
"older": "Mais antigo",
|
||||||
|
"oneByteMode0": "1 byte (modo 0)",
|
||||||
|
"oneHour": "1 hora",
|
||||||
|
"oneMinute": "1 minuto",
|
||||||
|
"openPacketLogs": "Abrir registos de pacotes",
|
||||||
|
"packetTypeHelp": "Ajuda sobre tipos de pacotes",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Colar um link de partilha ou anúncio hex",
|
||||||
|
"pathHash": "Hash do caminho",
|
||||||
|
"pathSize": "Tamanho do caminho",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Polígono",
|
||||||
|
"rawResponseCopied": "Resposta bruta copiada",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Reenviar pelo repetidor mais próximo em caso de falha",
|
||||||
|
"resolveContact": "Resolver contato",
|
||||||
|
"routingRetriesAndDestinationLock": "Encaminhamento, tentativas e bloqueio de destino",
|
||||||
|
"samples": "Amostras",
|
||||||
|
"satellites": "Satélites",
|
||||||
|
"saving": "A guardar...",
|
||||||
|
"search": "Pesquisar",
|
||||||
|
"searchByNameOrToken": "Pesquisar por nome ou token",
|
||||||
|
"searchDiscoveredNodes": "Pesquisar nós descobertos",
|
||||||
|
"searchInCurrentFilter": "Pesquisar no filtro atual",
|
||||||
|
"searchLogs": "Pesquisar registos...",
|
||||||
|
"searchSensors": "Pesquisar sensores",
|
||||||
|
"searchThisSection": "Pesquisar nesta secção",
|
||||||
|
"selectedRelays": "Repetidores selecionados",
|
||||||
|
"sensor": "Sensor",
|
||||||
|
"serialError": "Erro de série: {error}",
|
||||||
|
"setPathForContact": "Definir caminho para {name}",
|
||||||
|
"shareLiveGps": "Partilhar GPS ao vivo",
|
||||||
|
"shareMyTiles": "Partilhar os meus mosaicos",
|
||||||
|
"sharingLocationFromPhone": "A partilhar localização neste canal a partir do telemóvel.",
|
||||||
|
"sharingLocationFromRadio": "A partilhar localização neste canal a partir do rádio.",
|
||||||
|
"signalDbm": "Sinal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Tamanho, compressão e pré-visualização",
|
||||||
|
"skip": "Saltar",
|
||||||
|
"smallerFileSize": "Tamanho de ficheiro menor",
|
||||||
|
"sort": "Ordenar",
|
||||||
|
"stoppedSharingLocation": "Deixou de partilhar localização neste canal.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Suprimir notificações em primeiro plano",
|
||||||
|
"tabsAndNavigation": "Separadores e Navegação",
|
||||||
|
"templatesAndTutorials": "Modelos e tutoriais",
|
||||||
|
"tenMinutes": "10 minutos",
|
||||||
|
"tenSeconds": "10 segundos",
|
||||||
|
"testSendUpdate": "Testar envio de atualização",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, idioma e preferências de apresentação",
|
||||||
|
"thirtyMinutes": "30 minutos",
|
||||||
|
"thirtySeconds": "30 segundos",
|
||||||
|
"threeBytesMode2": "3 bytes (modo 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Estatísticas de tráfego, histórico de mensagens e ferramentas de programador",
|
||||||
|
"twoBytesMode1": "2 bytes (modo 1)",
|
||||||
|
"twoMinutes": "2 minutos",
|
||||||
|
"undo": "Desfazer",
|
||||||
|
"updated": "Atualizado",
|
||||||
|
"useContactFlags": "Usar bandeiras de contato",
|
||||||
|
"useSavedCoordinates": "Usar coordenadas guardadas",
|
||||||
|
"view": "Ver",
|
||||||
|
"viewNeighbours": "Ver vizinhos",
|
||||||
|
"viewPublicStats": "Ver estatísticas públicas",
|
||||||
|
"setRegionScope": "Definir âmbito de região",
|
||||||
|
"regionScope": "Âmbito de região",
|
||||||
|
"regionScopeNone": "Nenhum (global)",
|
||||||
|
"clearRegionScope": "Limpar âmbito",
|
||||||
|
"regionScopeWarning": "Apenas os repetidores que permitem esta região irão retransmitir.",
|
||||||
|
"discoverRegions": "Descobrir a partir dos repetidores",
|
||||||
|
"discoveringRegions": "A descobrir regiões...",
|
||||||
|
"enterRegionName": "Nome da região (ex. auckland)",
|
||||||
|
"noRegionsFound": "Nenhuma região encontrada neste repetidor.",
|
||||||
|
"regionScopeSet": "Âmbito de região definido para {name}",
|
||||||
|
"regionScopeCleared": "Âmbito de região limpo",
|
||||||
|
"simpleMode": "Modo simples",
|
||||||
|
"simpleModeDescription": "Apenas chat e mapa com contatos favoritos",
|
||||||
|
"simpleModeRequiresLockedDestination": "Ative primeiro \"Bloquear destino\" com um canal para usar o modo simples",
|
||||||
|
"noFavouriteContacts": "Nenhum contato favorito",
|
||||||
|
"selectDestinationFirst": "Selecione primeiro um canal, contato ou sala",
|
||||||
|
"recipientNoLongerAvailable": "O destinatário não está mais disponível",
|
||||||
|
"failedToSendMessage": "Falha ao enviar: {error}",
|
||||||
|
"tictactoeDirectOnly": "O jogo da velha funciona apenas em mensagens diretas. Escolha primeiro um contato.",
|
||||||
|
"deviceKeyUnavailable": "A chave do dispositivo não está disponível",
|
||||||
|
"imageCompressionFailed": "Falha na compressão da imagem",
|
||||||
|
"imageFragmentationFailed": "Falha na fragmentação da imagem",
|
||||||
|
"failedToAnnounceImage": "Falha ao anunciar a imagem",
|
||||||
|
"noRecipientSelected": "Nenhum destinatário selecionado",
|
||||||
|
"imageSendFailed": "Falha no envio da imagem",
|
||||||
|
"microphonePermissionRequiredForVoice": "É necessária permissão de microfone para mensagens de voz",
|
||||||
|
"recordingDiscarded": "Gravação descartada",
|
||||||
|
"publicChannelMediaWarning": "Você está prestes a enviar {mediaType} para o canal público. Isso não é recomendado, pois todos na rede mesh podem receber. Escolha um canal privado ou com tag, a menos que seja isso que você quer.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Selecione um canal privado para compartilhar sua localização.",
|
||||||
|
"stopSharingMyLocation": "Parar de compartilhar minha localização",
|
||||||
|
"shareMyLocation": "Compartilhar minha localização",
|
||||||
|
"stopRecording": "Parar gravação",
|
||||||
|
"recordVoice": "Gravar mensagem de voz",
|
||||||
|
"moreActions": "Mais ações",
|
||||||
|
"noMessagesInFilter": "Nenhuma mensagem neste filtro",
|
||||||
|
"noMatchesInFilter": "Nenhum resultado neste filtro",
|
||||||
|
"syncFailed": "Falha na sincronização: {error}",
|
||||||
|
"invalidLink": "Link inválido",
|
||||||
|
"cannotOpenLink": "Não é possível abrir o link",
|
||||||
|
"failedToOpenLink": "Falha ao abrir o link",
|
||||||
|
"failedToResendMessage": "Falha ao reenviar a mensagem",
|
||||||
|
"retryFailed": "Falha na nova tentativa: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Excluir desenho e mensagem",
|
||||||
|
"lastEchoRelay": "Último eco: repetidor",
|
||||||
|
"lastEchoPath": "Último eco: caminho",
|
||||||
|
"lastEchoBytesReport": "Último eco: relatório de bytes",
|
||||||
|
"noGpsCoordinatesAvailable": "Nenhuma coordenada GPS disponível",
|
||||||
|
"notASarMarker": "Não é um marcador SAR",
|
||||||
|
"failedToSaveTemplate": "Falha ao salvar o modelo: {error}",
|
||||||
|
"drawingNotFound": "Desenho não encontrado",
|
||||||
|
"unknownDrawingType": "Tipo de desenho desconhecido",
|
||||||
|
"customMapMarker": "Marcador de mapa personalizado",
|
||||||
|
"customMapPointLabel": "Ponto: {point}",
|
||||||
|
"mapIdLabel": "ID do mapa: {id}",
|
||||||
|
"cannotFetchVoice": "Não é possível obter a voz",
|
||||||
|
"senderContactUnknown": "O contato do remetente é desconhecido. Sincronize os contatos primeiro.",
|
||||||
|
"senderRouteUnknown": "A rota do remetente é desconhecida. Sincronize os contatos/o caminho primeiro.",
|
||||||
|
"messageTooFar": "A mensagem está longe demais ({hops} saltos, máx. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "A rota do remetente não respondeu à verificação de caminho. Sincronize os contatos/o caminho e tente novamente.",
|
||||||
|
"senderRouteNoRawResponse": "A rota do remetente não respondeu no caminho de transporte raw.",
|
||||||
|
"voiceFetchOverHops": "Obter a voz por {hops} saltos pode demorar um pouco.",
|
||||||
|
"voiceReceiveCanceled": "Recebimento de voz cancelado",
|
||||||
|
"fetchingMissingVoiceFragments": "Obtendo fragmentos de voz ausentes",
|
||||||
|
"receivingVoice": "Recebendo voz",
|
||||||
|
"cannotFetchImage": "Não é possível obter a imagem",
|
||||||
|
"imageFetchOverHops": "Obter a imagem por {hops} saltos pode demorar um pouco.",
|
||||||
|
"imageAlreadyBeingReceived": "A imagem já está sendo recebida",
|
||||||
|
"loadImage": "Carregar imagem",
|
||||||
|
"imageFetchFailedToSendRequest": "Falha ao enviar a solicitação da imagem",
|
||||||
|
"imageUnavailable": "Imagem indisponível no momento",
|
||||||
|
"imageFetchTimedOut": "Tempo esgotado ao obter a imagem",
|
||||||
|
"imageReceiveCanceled": "Recebimento da imagem cancelado",
|
||||||
|
"closeImagePreview": "Fechar visualização da imagem",
|
||||||
|
"recordingReleaseToSend": "Gravando... solte para enviar",
|
||||||
|
"sendingVoice": "Enviando voz...",
|
||||||
|
"sendLongPressToRecordVoice": "Enviar (toque longo para gravar voz)",
|
||||||
|
"discardRecording": "Descartar gravação",
|
||||||
|
"hopCount": "{count, plural, =1{1 salto} other{{count} saltos}}",
|
||||||
|
"channelsSectionDescription": "Canais de difusão para o tráfego mesh próximo",
|
||||||
|
"roomsSectionDescription": "Espaços compartilhados para coordenação contínua da equipe",
|
||||||
|
"contactsSectionDescription": "Pessoas e dispositivos ao seu alcance direto",
|
||||||
|
"noRecentChatters": "Sem participantes recentes",
|
||||||
|
"public": "Público",
|
||||||
|
"failedToSendTicTacToeMove": "Falha ao enviar a jogada do jogo da velha",
|
||||||
|
"youWon": "Você venceu",
|
||||||
|
"opponentWon": "O adversário venceu",
|
||||||
|
"gameDraw": "Empate",
|
||||||
|
"yourTurn": "Sua vez",
|
||||||
|
"opponentTurn": "Vez do adversário",
|
||||||
|
"pleaseEnterValidCoordinates": "Insira coordenadas válidas",
|
||||||
|
"locationNotAvailable": "Localização não disponível",
|
||||||
|
"customCaveMapPoint": "Ponto de mapa de caverna personalizado",
|
||||||
|
"mapPoint": "Ponto do mapa",
|
||||||
|
"fetchingMissingImageFragments": "Obtendo fragmentos ausentes…",
|
||||||
|
"loadingImage": "Carregando…",
|
||||||
|
"receivingImage": "Recebendo…",
|
||||||
|
"tapToLoad": "Toque para carregar",
|
||||||
|
"connectDevice": "Conectar dispositivo",
|
||||||
|
"chooseTransportSubtitle": "Escolha o transporte Bluetooth, WiFi ou Serial",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Rede",
|
||||||
|
"serial": "Serial",
|
||||||
|
"scan": "Escanear",
|
||||||
|
"pressScanToSearchForDevices": "Pressione escanear para procurar dispositivos próximos",
|
||||||
|
"failedToConnectToDevice": "Falha ao conectar a {name}",
|
||||||
|
"unknownDevice": "Dispositivo desconhecido",
|
||||||
|
"showingCachedResultsTapRefresh": "Exibindo resultados em cache. Toque em atualizar para escanear novamente.",
|
||||||
|
"scanningLocalNetworkOnPort": "Escaneando a rede local em busca de dispositivos MeshCore WiFi na porta {port}",
|
||||||
|
"cancelScanAndAddServer": "Cancelar escaneamento e adicionar servidor",
|
||||||
|
"addServer": "Adicionar servidor",
|
||||||
|
"scanningProgressIps": "Escaneando... {scanned}/{total} IPs",
|
||||||
|
"cancelAndEnterManually": "Cancelar e inserir manualmente",
|
||||||
|
"noRecentOrDiscoveredServers": "Ainda não há servidores recentes ou descobertos",
|
||||||
|
"recentlyUsed": "Usados recentemente",
|
||||||
|
"connecting": "Conectando...",
|
||||||
|
"discoveredOnThisNetwork": "Descobertos nesta rede",
|
||||||
|
"serverNoLongerAvailable": "O servidor em {host}:{port} não está mais disponível. Escaneie novamente para encontrar servidores ativos.",
|
||||||
|
"failedToConnectToHost": "Falha ao conectar a {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Insira um endereço IP válido",
|
||||||
|
"enterValidTcpPort": "Insira uma porta TCP válida",
|
||||||
|
"ipAddress": "Endereço IP",
|
||||||
|
"tcpPort": "Porta TCP",
|
||||||
|
"customServerPort": "Porta de servidor personalizada",
|
||||||
|
"chat": "Chat",
|
||||||
|
"restoringPreviousLink": "Restaurando a conexão anterior",
|
||||||
|
"noDeviceConnected": "Nenhum dispositivo conectado",
|
||||||
|
"sendAdvert": "Enviar meu contato",
|
||||||
|
"advertMode": "Enviar meu contato",
|
||||||
|
"advertModeSubtitle": "Escolha até onde suas informações de contato devem chegar.",
|
||||||
|
"floodAdvertSent": "Contato enviado por toda a rede mesh",
|
||||||
|
"directAdvertSent": "Contato enviado para os dispositivos próximos"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"disconnect": "Отключить",
|
"disconnect": "Отключить",
|
||||||
"noDevicesFound": "Устройства не найдены",
|
"noDevicesFound": "Устройства не найдены",
|
||||||
"scanAgain": "Повторить поиск",
|
"scanAgain": "Повторить поиск",
|
||||||
"tapToConnect": "Нажмите для подключения",
|
|
||||||
"deviceNotConnected": "Устройство не подключено",
|
"deviceNotConnected": "Устройство не подключено",
|
||||||
"locationPermissionDenied": "Доступ к геолокации запрещён",
|
"locationPermissionDenied": "Доступ к геолокации запрещён",
|
||||||
"locationPermissionPermanentlyDenied": "Доступ к геолокации запрещён навсегда. Включите его в настройках.",
|
"locationPermissionPermanentlyDenied": "Доступ к геолокации запрещён навсегда. Включите его в настройках.",
|
||||||
@@ -32,9 +31,7 @@
|
|||||||
"autoSystem": "Авто (Система)",
|
"autoSystem": "Авто (Система)",
|
||||||
"followSystemTheme": "Следовать системной теме",
|
"followSystemTheme": "Следовать системной теме",
|
||||||
"showRxTxIndicators": "Показывать индикаторы RX/TX",
|
"showRxTxIndicators": "Показывать индикаторы RX/TX",
|
||||||
"displayPacketActivity": "Отображать активность пакетов в верхней панели",
|
|
||||||
"disableMap": "Отключить карту",
|
"disableMap": "Отключить карту",
|
||||||
"disableMapDescription": "Скрыть вкладку карты для экономии заряда батареи",
|
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"chooseLanguage": "Выбрать язык",
|
"chooseLanguage": "Выбрать язык",
|
||||||
"save": "Сохранить",
|
"save": "Сохранить",
|
||||||
@@ -42,7 +39,6 @@
|
|||||||
"close": "Закрыть",
|
"close": "Закрыть",
|
||||||
"about": "О приложении",
|
"about": "О приложении",
|
||||||
"appVersion": "Версия приложения",
|
"appVersion": "Версия приложения",
|
||||||
"appName": "Название приложения",
|
|
||||||
"aboutMeshCoreSar": "О MeshCore SAR",
|
"aboutMeshCoreSar": "О MeshCore SAR",
|
||||||
"aboutDescription": "Приложение для поисково-спасательных операций, разработанное для аварийно-спасательных служб. Возможности:\n\n• BLE mesh-сеть для связи между устройствами\n• Офлайн-карты с несколькими вариантами слоёв\n• Отслеживание членов команды в реальном времени\n• Тактические маркеры SAR (найденный человек, пожар, место сбора)\n• Управление контактами и обмен сообщениями\n• GPS-трекинг с показанием курса компаса\n• Кэширование тайлов карт для работы офлайн",
|
"aboutDescription": "Приложение для поисково-спасательных операций, разработанное для аварийно-спасательных служб. Возможности:\n\n• BLE mesh-сеть для связи между устройствами\n• Офлайн-карты с несколькими вариантами слоёв\n• Отслеживание членов команды в реальном времени\n• Тактические маркеры SAR (найденный человек, пожар, место сбора)\n• Управление контактами и обмен сообщениями\n• GPS-трекинг с показанием курса компаса\n• Кэширование тайлов карт для работы офлайн",
|
||||||
"technologiesUsed": "Используемые технологии:",
|
"technologiesUsed": "Используемые технологии:",
|
||||||
@@ -69,7 +65,6 @@
|
|||||||
"viewOnMap": "Показать на карте",
|
"viewOnMap": "Показать на карте",
|
||||||
"refresh": "Обновить",
|
"refresh": "Обновить",
|
||||||
"resetPath": "Сбросить маршрут (перепроложить)",
|
"resetPath": "Сбросить маршрут (перепроложить)",
|
||||||
"publicKeyCopied": "Публичный ключ скопирован в буфер обмена",
|
|
||||||
"copiedToClipboard": "{label} скопировано в буфер обмена",
|
"copiedToClipboard": "{label} скопировано в буфер обмена",
|
||||||
"pleaseEnterPassword": "Пожалуйста, введите пароль",
|
"pleaseEnterPassword": "Пожалуйста, введите пароль",
|
||||||
"failedToSyncContacts": "Не удалось синхронизировать контакты: {error}",
|
"failedToSyncContacts": "Не удалось синхронизировать контакты: {error}",
|
||||||
@@ -149,15 +144,9 @@
|
|||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"publicKey": "Публичный ключ",
|
"publicKey": "Публичный ключ",
|
||||||
"lastSeen": "Последнее появление",
|
"lastSeen": "Последнее появление",
|
||||||
"roomStatus": "Статус комнаты",
|
|
||||||
"loginStatus": "Статус входа",
|
|
||||||
"notLoggedIn": "Не выполнен вход",
|
|
||||||
"adminAccess": "Доступ администратора",
|
|
||||||
"yes": "Да",
|
"yes": "Да",
|
||||||
"no": "Нет",
|
"no": "Нет",
|
||||||
"permissions": "Разрешения",
|
"permissions": "Разрешения",
|
||||||
"passwordSaved": "Пароль сохранён",
|
|
||||||
"locationColon": "Местоположение:",
|
|
||||||
"telemetry": "Телеметрия",
|
"telemetry": "Телеметрия",
|
||||||
"voltage": "Напряжение",
|
"voltage": "Напряжение",
|
||||||
"battery": "Батарея",
|
"battery": "Батарея",
|
||||||
@@ -165,8 +154,6 @@
|
|||||||
"humidity": "Влажность",
|
"humidity": "Влажность",
|
||||||
"pressure": "Давление",
|
"pressure": "Давление",
|
||||||
"gpsTelemetry": "GPS (телеметрия)",
|
"gpsTelemetry": "GPS (телеметрия)",
|
||||||
"updated": "Обновлено",
|
|
||||||
"pathResetInfo": "Маршрут сброшен для {name}. Следующее сообщение найдёт новый путь.",
|
|
||||||
"reLoginToRoom": "Войти в комнату повторно",
|
"reLoginToRoom": "Войти в комнату повторно",
|
||||||
"heading": "Курс",
|
"heading": "Курс",
|
||||||
"elevation": "Высота",
|
"elevation": "Высота",
|
||||||
@@ -241,7 +228,6 @@
|
|||||||
"tapToViewOnMap": "Нажмите, чтобы открыть на карте",
|
"tapToViewOnMap": "Нажмите, чтобы открыть на карте",
|
||||||
"radioSettings": "Настройки радио",
|
"radioSettings": "Настройки радио",
|
||||||
"frequencyMHz": "Частота (МГц)",
|
"frequencyMHz": "Частота (МГц)",
|
||||||
"frequencyExample": "например, 869.618",
|
|
||||||
"bandwidth": "Полоса пропускания",
|
"bandwidth": "Полоса пропускания",
|
||||||
"spreadingFactor": "Коэффициент расширения",
|
"spreadingFactor": "Коэффициент расширения",
|
||||||
"codingRate": "Скорость кодирования",
|
"codingRate": "Скорость кодирования",
|
||||||
@@ -264,38 +250,20 @@
|
|||||||
"noTrailRecorded": "Трек ещё не записан",
|
"noTrailRecorded": "Трек ещё не записан",
|
||||||
"startTrackingToRecord": "Запустите отслеживание геолокации для записи трека",
|
"startTrackingToRecord": "Запустите отслеживание геолокации для записи трека",
|
||||||
"trailControls": "Управление треком",
|
"trailControls": "Управление треком",
|
||||||
"contactTrails": "Треки контактов",
|
|
||||||
"showAllContactTrails": "Показать все треки контактов",
|
|
||||||
"noContactsWithLocationHistory": "Нет контактов с историей местоположения",
|
|
||||||
"showingTrailsForContacts": "Показаны треки для {count} контактов",
|
|
||||||
"individualContactTrails": "Индивидуальные треки контактов",
|
|
||||||
"deviceInformation": "Информация об устройстве",
|
|
||||||
"bleName": "Имя BLE",
|
|
||||||
"meshName": "Имя в сети Mesh",
|
|
||||||
"notSet": "Не задано",
|
|
||||||
"model": "Модель",
|
"model": "Модель",
|
||||||
"version": "Версия",
|
"version": "Версия",
|
||||||
"buildDate": "Дата сборки",
|
|
||||||
"firmware": "Прошивка",
|
|
||||||
"maxContacts": "Макс. контактов",
|
"maxContacts": "Макс. контактов",
|
||||||
"maxChannels": "Макс. каналов",
|
"maxChannels": "Макс. каналов",
|
||||||
"publicInfo": "Публичная информация",
|
"publicInfo": "Публичная информация",
|
||||||
"meshNetworkName": "Название сети Mesh",
|
|
||||||
"nameBroadcastInMesh": "Имя, транслируемое в mesh-объявлениях",
|
|
||||||
"telemetryAndLocationSharing": "Телеметрия и передача местоположения",
|
|
||||||
"lat": "Ш",
|
|
||||||
"lon": "Д",
|
|
||||||
"useCurrentLocation": "Использовать текущее местоположение",
|
"useCurrentLocation": "Использовать текущее местоположение",
|
||||||
"noneUnknown": "Нет/Неизвестно",
|
"noneUnknown": "Нет/Неизвестно",
|
||||||
"chatNode": "Узел чата",
|
"chatNode": "Узел чата",
|
||||||
"repeater": "Ретранслятор",
|
"repeater": "Ретранслятор",
|
||||||
"roomChannel": "Комната/Канал",
|
"roomChannel": "Комната/Канал",
|
||||||
"typeNumber": "Тип {number}",
|
"typeNumber": "Тип {number}",
|
||||||
"copiedToClipboardShort": "{label} скопировано в буфер обмена",
|
|
||||||
"failedToSave": "Не удалось сохранить: {error}",
|
"failedToSave": "Не удалось сохранить: {error}",
|
||||||
"failedToGetLocation": "Не удалось получить местоположение: {error}",
|
"failedToGetLocation": "Не удалось получить местоположение: {error}",
|
||||||
"sarTemplates": "Шаблоны SAR",
|
"sarTemplates": "Шаблоны SAR",
|
||||||
"manageSarTemplates": "Управление шаблонами целеуказания",
|
|
||||||
"addTemplate": "Добавить шаблон",
|
"addTemplate": "Добавить шаблон",
|
||||||
"editTemplate": "Изменить шаблон",
|
"editTemplate": "Изменить шаблон",
|
||||||
"deleteTemplate": "Удалить шаблон",
|
"deleteTemplate": "Удалить шаблон",
|
||||||
@@ -323,7 +291,6 @@
|
|||||||
"noTemplates": "Шаблоны недоступны",
|
"noTemplates": "Шаблоны недоступны",
|
||||||
"tapAddToCreate": "Нажмите +, чтобы создать первый шаблон",
|
"tapAddToCreate": "Нажмите +, чтобы создать первый шаблон",
|
||||||
"ok": "ОК",
|
"ok": "ОК",
|
||||||
"permissionsSection": "Разрешения",
|
|
||||||
"locationPermission": "Разрешение геолокации",
|
"locationPermission": "Разрешение геолокации",
|
||||||
"checking": "Проверка...",
|
"checking": "Проверка...",
|
||||||
"locationPermissionGrantedAlways": "Разрешено (всегда)",
|
"locationPermissionGrantedAlways": "Разрешено (всегда)",
|
||||||
@@ -460,28 +427,6 @@
|
|||||||
"wizardNext": "Далее",
|
"wizardNext": "Далее",
|
||||||
"wizardGetStarted": "Начать",
|
"wizardGetStarted": "Начать",
|
||||||
"wizardWelcomeTitle": "Добро пожаловать в MeshCore SAR",
|
"wizardWelcomeTitle": "Добро пожаловать в MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Мощный инструмент связи вне сети для поисково-спасательных операций. Поддерживайте связь с командой через mesh-радио, когда традиционные сети недоступны.",
|
|
||||||
"wizardConnectingTitle": "Подключение к радиоустройству",
|
|
||||||
"wizardConnectingDescription": "Подключите смартфон к радиоустройству MeshCore по Bluetooth для связи вне сети.",
|
|
||||||
"wizardConnectingFeature1": "Поиск ближайших устройств MeshCore",
|
|
||||||
"wizardConnectingFeature2": "Сопряжение с радиоустройством по Bluetooth",
|
|
||||||
"wizardConnectingFeature3": "Работает полностью офлайн — интернет не нужен",
|
|
||||||
"wizardChannelTitle": "Каналы",
|
|
||||||
"wizardChannelDescription": "Транслируйте сообщения всем участникам канала — идеально для общих объявлений и координации команды.",
|
|
||||||
"wizardChannelFeature1": "Публичный канал для общей связи команды",
|
|
||||||
"wizardChannelFeature2": "Создавайте пользовательские каналы для отдельных групп",
|
|
||||||
"wizardChannelFeature3": "Сообщения автоматически ретранслируются через mesh",
|
|
||||||
"wizardContactsTitle": "Контакты",
|
|
||||||
"wizardContactsDescription": "Члены вашей команды появляются автоматически по мере подключения к mesh-сети. Отправляйте им личные сообщения или просматривайте их местоположение.",
|
|
||||||
"wizardContactsFeature1": "Контакты обнаруживаются автоматически",
|
|
||||||
"wizardContactsFeature2": "Отправка личных сообщений",
|
|
||||||
"wizardContactsFeature3": "Просмотр уровня заряда и времени последнего появления",
|
|
||||||
"wizardMapTitle": "Карта и местоположение",
|
|
||||||
"wizardMapDescription": "Отслеживайте команду в реальном времени и отмечайте важные места для поисково-спасательных операций.",
|
|
||||||
"wizardMapFeature1": "Маркеры SAR для найденных людей, пожаров и мест сбора",
|
|
||||||
"wizardMapFeature2": "GPS-отслеживание членов команды в реальном времени",
|
|
||||||
"wizardMapFeature3": "Загрузка офлайн-карт для отдалённых районов",
|
|
||||||
"wizardMapFeature4": "Рисование фигур и обмен тактической информацией",
|
|
||||||
"viewWelcomeTutorial": "Просмотреть обучение",
|
"viewWelcomeTutorial": "Просмотреть обучение",
|
||||||
"allTeamContacts": "Все контакты команды",
|
"allTeamContacts": "Все контакты команды",
|
||||||
"directMessagesInfo": "Личные сообщения с подтверждением. Отправлено {count} членам команды.",
|
"directMessagesInfo": "Личные сообщения с подтверждением. Отправлено {count} членам команды.",
|
||||||
@@ -563,5 +508,525 @@
|
|||||||
"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": "Полосовой фильтр голоса",
|
||||||
|
"active": "Активно",
|
||||||
|
"addContact": "Добавить контакт",
|
||||||
|
"all": "Все",
|
||||||
|
"clearAllLabel": "Очистить все",
|
||||||
|
"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": "Поделиться",
|
||||||
|
"trace": "Трассировка",
|
||||||
|
"discovery": "Обнаружение",
|
||||||
|
"discoverRepeaters": "Обнаружить повторители",
|
||||||
|
"discoverSensors": "Обнаружить датчики",
|
||||||
|
"repeaterDiscoverySent": "Обнаружение повторителей отправлено",
|
||||||
|
"sensorDiscoverySent": "Обнаружение датчиков отправлено",
|
||||||
|
"clearedPendingDiscoveries": "Ожидающие обнаружения очищены.",
|
||||||
|
"autoDiscovery": "Автоматическое обнаружение",
|
||||||
|
"enableAutomaticAdding": "Включить автоматическое добавление",
|
||||||
|
"autoaddRepeaters": "Автодобавление повторителей",
|
||||||
|
"autoaddRoomServers": "Автодобавление серверов комнат",
|
||||||
|
"autoaddSensors": "Автодобавление датчиков",
|
||||||
|
"autoaddUsers": "Автодобавление пользователей",
|
||||||
|
"overwriteOldestWhenFull": "Перезаписать старейшие при заполнении",
|
||||||
|
"storage": "Хранилище",
|
||||||
|
"dangerZone": "Опасная зона",
|
||||||
|
"profiles": "Профили",
|
||||||
|
"favourites": "Избранное",
|
||||||
|
"sensors": "Датчики",
|
||||||
|
"others": "Другие",
|
||||||
|
"gpsModule": "Модуль GPS",
|
||||||
|
"liveTraffic": "Живой трафик",
|
||||||
|
"repeatersMap": "Карта повторителей",
|
||||||
|
"blePacketLogs": "Журналы BLE-пакетов",
|
||||||
|
"onlineTraceDatabase": "База трассировок",
|
||||||
|
"routePathByteSize": "Размер пути в байтах",
|
||||||
|
"messageNotifications": "Уведомления о сообщениях",
|
||||||
|
"sarAlerts": "SAR-оповещения",
|
||||||
|
"discoveryNotifications": "Уведомления об обнаружении",
|
||||||
|
"updateNotifications": "Уведомления об обновлениях",
|
||||||
|
"muteWhileAppIsOpen": "Без звука при открытом приложении",
|
||||||
|
"disableContacts": "Отключить контакты",
|
||||||
|
"enableSensorsTab": "Включить вкладку Датчики",
|
||||||
|
"enableProfiles": "Включить профили",
|
||||||
|
"nearestRepeaterFallback": "Ближайший повторитель как резерв",
|
||||||
|
"deleteAllStoredMessageHistory": "Удалить всю историю сообщений",
|
||||||
|
"messageFontSize": "Размер шрифта сообщений",
|
||||||
|
"rotateMapWithHeading": "Вращать карту по направлению",
|
||||||
|
"showMapDebugInfo": "Показать отладочную информацию карты",
|
||||||
|
"openMapInFullscreen": "Открыть карту на весь экран",
|
||||||
|
"showSarMarkersLabel": "Показать SAR-маркеры",
|
||||||
|
"hideRepeatersOnMap": "Скрыть повторители на карте",
|
||||||
|
"setMapScale": "Установить масштаб карты",
|
||||||
|
"customMapScaleSaved": "Пользовательский масштаб карты сохранён",
|
||||||
|
"voiceBitrate": "Битрейт голоса",
|
||||||
|
"voiceCompressor": "Компрессор голоса",
|
||||||
|
"voiceLimiter": "Лимитер голоса",
|
||||||
|
"micAutoGain": "Автоусиление микрофона",
|
||||||
|
"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": "Тихо",
|
||||||
|
"busy": "Занято",
|
||||||
|
"searchMessages": "Поиск сообщений",
|
||||||
|
"sendImageFromGallery": "Отправить изображение из галереи",
|
||||||
|
"takePhoto": "Сделать фото",
|
||||||
|
"allMessages": "Все сообщения",
|
||||||
|
"sendToPublicChannel": "Отправить в публичный канал?",
|
||||||
|
"selectMarkerTypeAndDestination": "Выберите тип маркера и назначение",
|
||||||
|
"noDestinationsAvailableLabel": "Нет доступных назначений",
|
||||||
|
"image": "Изображение",
|
||||||
|
"format": "Формат",
|
||||||
|
"dimensions": "Размеры",
|
||||||
|
"segments": "Сегменты",
|
||||||
|
"transfers": "Передачи",
|
||||||
|
"downloadedBy": "Загружено",
|
||||||
|
"saveDiscoverySettings": "Сохранить настройки обнаружения",
|
||||||
|
"savePublicInfo": "Сохранить публичную информацию",
|
||||||
|
"saveRadioSettings": "Сохранить настройки радио",
|
||||||
|
"savePath": "Сохранить путь",
|
||||||
|
"wipeDeviceData": "Стереть данные устройства",
|
||||||
|
"wipeDevice": "Стереть устройство",
|
||||||
|
"destructiveDeviceActions": "Деструктивные действия устройства.",
|
||||||
|
"chooseAPresetOrFinetuneCustomRadioSettings": "Выберите пресет или настройте радио вручную.",
|
||||||
|
"used": "Использовано",
|
||||||
|
"total": "Всего",
|
||||||
|
"renameValue": "Переименовать значение",
|
||||||
|
"customizeFields": "Настроить поля",
|
||||||
|
"livePreview": "Предпросмотр в реальном времени",
|
||||||
|
"refreshSchedule": "Расписание обновления",
|
||||||
|
"noResponse": "Нет ответа",
|
||||||
|
"refreshing": "Обновление",
|
||||||
|
"unavailable": "Недоступно",
|
||||||
|
"pickARelayOrNodeToWatchInSensors": "Выберите реле или узел для наблюдения.",
|
||||||
|
"publicKeyLabel": "Публичный ключ",
|
||||||
|
"alreadyInContacts": "Уже в контактах",
|
||||||
|
"connectToADeviceBeforeAddingContacts": "Подключитесь к устройству перед добавлением контактов",
|
||||||
|
"fromContacts": "Из контактов",
|
||||||
|
"onlineOnly": "Только онлайн",
|
||||||
|
"inBoth": "В обоих",
|
||||||
|
"source": "Источник",
|
||||||
|
"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": "Нет контактов устройства для очистки.",
|
||||||
|
"autoSend": "Автоотправка",
|
||||||
|
"autoSendDescription": "Отправить текущим маршрутом.",
|
||||||
|
"sendDirect": "Прямая отправка",
|
||||||
|
"sendDirectDescription": "Отправить напрямую этому контакту.",
|
||||||
|
"sendFlood": "Отправка лавиной",
|
||||||
|
"sendFloodDescription": "Отправить через все ретрансляторы.",
|
||||||
|
"postConnectDiscoveryTitle": "Обнаружить ретрансляторы сейчас?",
|
||||||
|
"postConnectDiscoveryDescription": "Запустите обнаружение ретрансляторов сразу после подключения, чтобы увидеть ближайшие узлы MeshCore и быстрее добавить их в свою сеть.",
|
||||||
|
"aToZ": "А-Я",
|
||||||
|
"add": "Добавить",
|
||||||
|
"addAdditionalDetails": "Добавить дополнительные сведения",
|
||||||
|
"addPeer": "Добавить узел",
|
||||||
|
"addPeerManually": "Добавить узел вручную",
|
||||||
|
"addedToContacts": "{name} добавлен в контакты",
|
||||||
|
"addedToSensors": "{name} добавлен в Датчики",
|
||||||
|
"allowAll": "Разрешить все",
|
||||||
|
"anonymousRxStats": "Анонимная статистика RX",
|
||||||
|
"appearance": "Внешний вид",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Базовая телеметрия",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Звуковые оповещения",
|
||||||
|
"cancelImageReceive": "Отменить получение изображения",
|
||||||
|
"choose": "Выбрать",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Выберите отображаемые вкладки и разделы контактов",
|
||||||
|
"clearCache": "Очистить кэш",
|
||||||
|
"clearHistory": "Очистить историю",
|
||||||
|
"clearLiveView": "Очистить режим реального времени",
|
||||||
|
"clearLogs": "Очистить журналы",
|
||||||
|
"clearOfflineCache": "Очистить офлайн-кэш?",
|
||||||
|
"clearOverlay": "Очистить наложение",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Очищать маршрут только после всех неудачных повторов",
|
||||||
|
"codecBitrateAndAudioProcessing": "Кодек, битрейт и обработка звука",
|
||||||
|
"connectByIpAddress": "Подключиться по IP-адресу",
|
||||||
|
"controlWhichAlertsYouReceive": "Управление получаемыми оповещениями",
|
||||||
|
"copyFullHex": "Копировать полный Hex",
|
||||||
|
"copyRawResponse": "Копировать необработанный ответ",
|
||||||
|
"couldNotLoadMetHistory": "Не удалось загрузить историю MET",
|
||||||
|
"customizeSensor": "Настроить {name}",
|
||||||
|
"data": "Данные",
|
||||||
|
"deleteGroup": "Удалить группу",
|
||||||
|
"deleteStyleConfirm": "Удалить {name}?",
|
||||||
|
"deleteThisDrawing": "Удалить этот {type}?",
|
||||||
|
"deny": "Запретить",
|
||||||
|
"deviceInfo": "Информация об устройстве",
|
||||||
|
"deviceSettings": "Настройки устройства",
|
||||||
|
"deviceTimeSynced": "Время устройства синхронизировано с телефоном.",
|
||||||
|
"disableFastGpsPublishing": "Отключить быструю публикацию GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Отображение, маркеры и база трассировок",
|
||||||
|
"download": "Скачать",
|
||||||
|
"environmentalTelemetry": "Телеметрия окружающей среды",
|
||||||
|
"errorCheckingForUpdates": "Ошибка проверки обновлений: {error}",
|
||||||
|
"errorGeneric": "Ошибка: {error}",
|
||||||
|
"errorOpeningDownload": "Ошибка открытия загрузки: {error}",
|
||||||
|
"exportLogs": "Экспортировать журналы",
|
||||||
|
"extraAggressiveAvifCompression": "Сверхагрессивное сжатие AVIF",
|
||||||
|
"failedToAddContact": "Не удалось добавить контакт: {error}",
|
||||||
|
"failedToAddName": "Не удалось добавить {name}: {error}",
|
||||||
|
"failedToClearChannels": "Не удалось очистить каналы: {error}",
|
||||||
|
"failedToClearContacts": "Не удалось очистить контакты: {error}",
|
||||||
|
"failedToConnectViaSerial": "Не удалось подключиться через последовательный порт",
|
||||||
|
"failedToLoadPreviewImage": "Не удалось загрузить изображение предпросмотра: {error}",
|
||||||
|
"failedToLoadTrace": "Не удалось загрузить трассировку: {error}",
|
||||||
|
"failedToSendSarMarker": "Не удалось отправить маркер SAR: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Не удалось установить режим зуммера: {error}",
|
||||||
|
"failedToSetGpsMode": "Не удалось установить режим GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Не удалось синхронизировать время устройства: {error}",
|
||||||
|
"failedToWipeDeviceData": "Не удалось стереть данные устройства: {error}",
|
||||||
|
"fastGpsTargetChannel": "Целевой канал быстрого GPS",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Быстрые обновления, пороги и разрешения",
|
||||||
|
"fetchingCatalog": "Загрузка каталога...",
|
||||||
|
"fifteenMinutes": "15 минут",
|
||||||
|
"fifteenSeconds": "15 секунд",
|
||||||
|
"filterByDirection": "Фильтр по направлению",
|
||||||
|
"finish": "Завершить",
|
||||||
|
"fiveMinutes": "5 минут",
|
||||||
|
"fiveSeconds": "5 секунд",
|
||||||
|
"fix": "Фикс",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Политика объявлений GPS",
|
||||||
|
"gpsAndLocation": "GPS и местоположение",
|
||||||
|
"gpsDiagnostics": "Диагностика GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Помощь",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Скрыто",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Как радио автоматически добавляет обнаруженные узлы.",
|
||||||
|
"images": "Изображения",
|
||||||
|
"importProfile": "Импортировать профиль",
|
||||||
|
"lastFix": "Последний фикс",
|
||||||
|
"latest": "Последняя",
|
||||||
|
"latitude": "Широта",
|
||||||
|
"loadASavedRegion": "Загрузить сохранённый регион",
|
||||||
|
"loading": "Загрузка",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Обмен местоположением, оборудование и интервал обновления.",
|
||||||
|
"locationTelemetry": "Телеметрия местоположения",
|
||||||
|
"lockDestination": "Зафиксировать назначение",
|
||||||
|
"longitude": "Долгота",
|
||||||
|
"mapStyle": "Стиль карты",
|
||||||
|
"max": "Макс",
|
||||||
|
"maxZoom": "Макс. масштаб",
|
||||||
|
"messaging": "Сообщения",
|
||||||
|
"metHistory": "История MET",
|
||||||
|
"min": "Мин",
|
||||||
|
"minZoom": "Мин. масштаб",
|
||||||
|
"moveDown": "Переместить вниз",
|
||||||
|
"moveUp": "Переместить вверх",
|
||||||
|
"multiAckMode": "Режим множественного ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Управление рабочим пространством на нескольких устройствах",
|
||||||
|
"myLocation": "Моё местоположение",
|
||||||
|
"nameAndTelemetryShared": "Имя и телеметрия доступны другим устройствам.",
|
||||||
|
"newVersionAvailable": "Новая версия: {version}",
|
||||||
|
"newer": "Новее",
|
||||||
|
"noNeighboursFound": "Соседи не найдены",
|
||||||
|
"noServersFound": "Серверы не найдены",
|
||||||
|
"notSet": "Не задано",
|
||||||
|
"notifications": "Уведомления",
|
||||||
|
"offZeroSeconds": "Выкл (0 с)",
|
||||||
|
"offlineMaps": "Офлайн-карты",
|
||||||
|
"older": "Старее",
|
||||||
|
"oneByteMode0": "1 байт (режим 0)",
|
||||||
|
"oneHour": "1 час",
|
||||||
|
"oneMinute": "1 минута",
|
||||||
|
"openPacketLogs": "Открыть журналы пакетов",
|
||||||
|
"packetTypeHelp": "Справка по типам пакетов",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Вставьте ссылку или Hex-объявление",
|
||||||
|
"pathHash": "Хеш пути",
|
||||||
|
"pathSize": "Размер пути",
|
||||||
|
"ping": "Пинг",
|
||||||
|
"polygon": "Полигон",
|
||||||
|
"rawResponseCopied": "Необработанный ответ скопирован",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Повторить через ближайший ретранслятор при ошибке",
|
||||||
|
"resolveContact": "Разрешить контакт",
|
||||||
|
"routingRetriesAndDestinationLock": "Маршрутизация, повторы и фиксация назначения",
|
||||||
|
"samples": "Образцы",
|
||||||
|
"satellites": "Спутники",
|
||||||
|
"saving": "Сохранение...",
|
||||||
|
"search": "Поиск",
|
||||||
|
"searchByNameOrToken": "Поиск по имени или токену",
|
||||||
|
"searchDiscoveredNodes": "Поиск обнаруженных узлов",
|
||||||
|
"searchInCurrentFilter": "Поиск в текущем фильтре",
|
||||||
|
"searchLogs": "Поиск по журналам...",
|
||||||
|
"searchSensors": "Поиск датчиков",
|
||||||
|
"searchThisSection": "Поиск в этом разделе",
|
||||||
|
"selectedRelays": "Выбранные ретрансляторы",
|
||||||
|
"sensor": "Датчик",
|
||||||
|
"serialError": "Ошибка последовательного порта: {error}",
|
||||||
|
"setPathForContact": "Задать путь для {name}",
|
||||||
|
"shareLiveGps": "Транслировать GPS",
|
||||||
|
"shareMyTiles": "Поделиться моими тайлами",
|
||||||
|
"sharingLocationFromPhone": "Трансляция местоположения на этом канале с телефона.",
|
||||||
|
"sharingLocationFromRadio": "Трансляция местоположения на этом канале с радио.",
|
||||||
|
"signalDbm": "Сигнал {rssi} дБм",
|
||||||
|
"sizeCompressionAndPreview": "Размер, сжатие и предпросмотр",
|
||||||
|
"skip": "Пропустить",
|
||||||
|
"smallerFileSize": "Меньший размер файла",
|
||||||
|
"sort": "Сортировка",
|
||||||
|
"stoppedSharingLocation": "Трансляция местоположения на этом канале остановлена.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Отключить уведомления при открытом приложении",
|
||||||
|
"tabsAndNavigation": "Вкладки и навигация",
|
||||||
|
"templatesAndTutorials": "Шаблоны и обучение",
|
||||||
|
"tenMinutes": "10 минут",
|
||||||
|
"tenSeconds": "10 секунд",
|
||||||
|
"testSendUpdate": "Тестовая отправка обновления",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Тема, язык и настройки отображения",
|
||||||
|
"thirtyMinutes": "30 минут",
|
||||||
|
"thirtySeconds": "30 секунд",
|
||||||
|
"threeBytesMode2": "3 байта (режим 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Статистика трафика, история сообщений и инструменты разработчика",
|
||||||
|
"twoBytesMode1": "2 байта (режим 1)",
|
||||||
|
"twoMinutes": "2 минуты",
|
||||||
|
"undo": "Отменить",
|
||||||
|
"updated": "Обновлено",
|
||||||
|
"useContactFlags": "Использовать флаги контактов",
|
||||||
|
"useSavedCoordinates": "Использовать сохранённые координаты",
|
||||||
|
"view": "Просмотр",
|
||||||
|
"viewNeighbours": "Просмотр соседей",
|
||||||
|
"viewPublicStats": "Просмотр публичной статистики",
|
||||||
|
"setRegionScope": "Установить область региона",
|
||||||
|
"regionScope": "Область региона",
|
||||||
|
"regionScopeNone": "Нет (глобально)",
|
||||||
|
"clearRegionScope": "Очистить область",
|
||||||
|
"regionScopeWarning": "Только ретрансляторы, разрешающие этот регион, будут пересылать.",
|
||||||
|
"discoverRegions": "Обнаружить из ретрансляторов",
|
||||||
|
"discoveringRegions": "Обнаружение регионов...",
|
||||||
|
"enterRegionName": "Название региона (напр. auckland)",
|
||||||
|
"noRegionsFound": "Регионы не найдены на этом ретрансляторе.",
|
||||||
|
"regionScopeSet": "Область региона установлена на {name}",
|
||||||
|
"regionScopeCleared": "Область региона очищена",
|
||||||
|
"simpleMode": "Простой режим",
|
||||||
|
"simpleModeDescription": "Только чат и карта с избранными контактами",
|
||||||
|
"simpleModeRequiresLockedDestination": "Сначала включите «Зафиксировать назначение» с каналом, чтобы использовать простой режим",
|
||||||
|
"noFavouriteContacts": "Нет избранных контактов",
|
||||||
|
"selectDestinationFirst": "Сначала выберите канал, контакт или комнату",
|
||||||
|
"recipientNoLongerAvailable": "Получатель больше не доступен",
|
||||||
|
"failedToSendMessage": "Не удалось отправить: {error}",
|
||||||
|
"tictactoeDirectOnly": "Крестики-нолики работают только в личных сообщениях. Сначала выберите контакт.",
|
||||||
|
"deviceKeyUnavailable": "Ключ устройства недоступен",
|
||||||
|
"imageCompressionFailed": "Не удалось сжать изображение",
|
||||||
|
"imageFragmentationFailed": "Не удалось разделить изображение на фрагменты",
|
||||||
|
"failedToAnnounceImage": "Не удалось анонсировать изображение",
|
||||||
|
"noRecipientSelected": "Получатель не выбран",
|
||||||
|
"imageSendFailed": "Не удалось отправить изображение",
|
||||||
|
"microphonePermissionRequiredForVoice": "Для голосовых сообщений требуется разрешение на микрофон",
|
||||||
|
"recordingDiscarded": "Запись отменена",
|
||||||
|
"publicChannelMediaWarning": "Вы собираетесь отправить {mediaType} в публичный канал. Это не рекомендуется: получить это может любой в mesh-сети. Выберите приватный или тегированный канал, если только это не то, что вы хотите.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Выберите приватный канал, чтобы поделиться местоположением.",
|
||||||
|
"stopSharingMyLocation": "Прекратить делиться местоположением",
|
||||||
|
"shareMyLocation": "Поделиться местоположением",
|
||||||
|
"stopRecording": "Остановить запись",
|
||||||
|
"recordVoice": "Записать голосовое сообщение",
|
||||||
|
"moreActions": "Другие действия",
|
||||||
|
"noMessagesInFilter": "В этом фильтре нет сообщений",
|
||||||
|
"noMatchesInFilter": "В этом фильтре нет совпадений",
|
||||||
|
"syncFailed": "Ошибка синхронизации: {error}",
|
||||||
|
"invalidLink": "Недействительная ссылка",
|
||||||
|
"cannotOpenLink": "Не удаётся открыть ссылку",
|
||||||
|
"failedToOpenLink": "Не удалось открыть ссылку",
|
||||||
|
"failedToResendMessage": "Не удалось повторно отправить сообщение",
|
||||||
|
"retryFailed": "Повторная попытка не удалась: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Удалить рисунок и сообщение",
|
||||||
|
"lastEchoRelay": "Последнее эхо: ретранслятор",
|
||||||
|
"lastEchoPath": "Последнее эхо: путь",
|
||||||
|
"lastEchoBytesReport": "Последнее эхо: отчёт о байтах",
|
||||||
|
"noGpsCoordinatesAvailable": "Координаты GPS недоступны",
|
||||||
|
"notASarMarker": "Это не маркер SAR",
|
||||||
|
"failedToSaveTemplate": "Не удалось сохранить шаблон: {error}",
|
||||||
|
"drawingNotFound": "Рисунок не найден",
|
||||||
|
"unknownDrawingType": "Неизвестный тип рисунка",
|
||||||
|
"customMapMarker": "Пользовательский маркер карты",
|
||||||
|
"customMapPointLabel": "Точка: {point}",
|
||||||
|
"mapIdLabel": "ID карты: {id}",
|
||||||
|
"cannotFetchVoice": "Не удаётся получить голосовое сообщение",
|
||||||
|
"senderContactUnknown": "Контакт отправителя неизвестен. Сначала синхронизируйте контакты.",
|
||||||
|
"senderRouteUnknown": "Маршрут отправителя неизвестен. Сначала синхронизируйте контакты/путь.",
|
||||||
|
"messageTooFar": "Сообщение слишком далеко ({hops} хопов, макс. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Маршрут отправителя не ответил на проверку пути. Синхронизируйте контакты/путь и попробуйте снова.",
|
||||||
|
"senderRouteNoRawResponse": "Маршрут отправителя не ответил на raw-транспортном пути.",
|
||||||
|
"voiceFetchOverHops": "Получение голоса через {hops} хопов может занять время.",
|
||||||
|
"voiceReceiveCanceled": "Приём голосового сообщения отменён",
|
||||||
|
"fetchingMissingVoiceFragments": "Получение недостающих фрагментов голоса",
|
||||||
|
"receivingVoice": "Приём голоса",
|
||||||
|
"cannotFetchImage": "Не удаётся получить изображение",
|
||||||
|
"imageFetchOverHops": "Получение изображения через {hops} хопов может занять время.",
|
||||||
|
"imageAlreadyBeingReceived": "Изображение уже принимается",
|
||||||
|
"loadImage": "Загрузить изображение",
|
||||||
|
"imageFetchFailedToSendRequest": "Не удалось отправить запрос на изображение",
|
||||||
|
"imageUnavailable": "Изображение сейчас недоступно",
|
||||||
|
"imageFetchTimedOut": "Время ожидания изображения истекло",
|
||||||
|
"imageReceiveCanceled": "Приём изображения отменён",
|
||||||
|
"closeImagePreview": "Закрыть просмотр изображения",
|
||||||
|
"recordingReleaseToSend": "Запись... отпустите, чтобы отправить",
|
||||||
|
"sendingVoice": "Отправка голоса...",
|
||||||
|
"sendLongPressToRecordVoice": "Отправить (удерживайте для записи голоса)",
|
||||||
|
"discardRecording": "Удалить запись",
|
||||||
|
"hopCount": "{count, plural, one{{count} хоп} few{{count} хопа} many{{count} хопов} other{{count} хопа}}",
|
||||||
|
"channelsSectionDescription": "Широковещательные каналы для mesh-трафика поблизости",
|
||||||
|
"roomsSectionDescription": "Общие пространства для постоянной координации команды",
|
||||||
|
"contactsSectionDescription": "Люди и устройства в прямой досягаемости",
|
||||||
|
"noRecentChatters": "Нет недавних собеседников",
|
||||||
|
"public": "Публичный",
|
||||||
|
"failedToSendTicTacToeMove": "Не удалось отправить ход в крестики-нолики",
|
||||||
|
"youWon": "Вы победили",
|
||||||
|
"opponentWon": "Победил соперник",
|
||||||
|
"gameDraw": "Ничья",
|
||||||
|
"yourTurn": "Ваш ход",
|
||||||
|
"opponentTurn": "Ход соперника",
|
||||||
|
"pleaseEnterValidCoordinates": "Введите корректные координаты",
|
||||||
|
"locationNotAvailable": "Местоположение недоступно",
|
||||||
|
"customCaveMapPoint": "Пользовательская точка карты пещеры",
|
||||||
|
"mapPoint": "Точка карты",
|
||||||
|
"fetchingMissingImageFragments": "Получение недостающих фрагментов…",
|
||||||
|
"loadingImage": "Загрузка…",
|
||||||
|
"receivingImage": "Приём…",
|
||||||
|
"tapToLoad": "Нажмите, чтобы загрузить",
|
||||||
|
"connectDevice": "Подключить устройство",
|
||||||
|
"chooseTransportSubtitle": "Выберите транспорт: Bluetooth, WiFi или последовательный порт",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Сеть",
|
||||||
|
"serial": "Последовательный порт",
|
||||||
|
"scan": "Сканировать",
|
||||||
|
"pressScanToSearchForDevices": "Нажмите «Сканировать», чтобы найти устройства поблизости",
|
||||||
|
"failedToConnectToDevice": "Не удалось подключиться к {name}",
|
||||||
|
"unknownDevice": "Неизвестное устройство",
|
||||||
|
"showingCachedResultsTapRefresh": "Показаны кэшированные результаты. Нажмите «Обновить», чтобы пересканировать.",
|
||||||
|
"scanningLocalNetworkOnPort": "Сканирование локальной сети на наличие WiFi-устройств MeshCore на порту {port}",
|
||||||
|
"cancelScanAndAddServer": "Отменить сканирование и добавить сервер",
|
||||||
|
"addServer": "Добавить сервер",
|
||||||
|
"scanningProgressIps": "Сканирование... {scanned}/{total} IP-адресов",
|
||||||
|
"cancelAndEnterManually": "Отменить и ввести вручную",
|
||||||
|
"noRecentOrDiscoveredServers": "Пока нет недавних или обнаруженных серверов",
|
||||||
|
"recentlyUsed": "Недавно использованные",
|
||||||
|
"connecting": "Подключение...",
|
||||||
|
"discoveredOnThisNetwork": "Обнаружены в этой сети",
|
||||||
|
"serverNoLongerAvailable": "Сервер {host}:{port} больше недоступен. Выполните сканирование снова, чтобы найти активные серверы.",
|
||||||
|
"failedToConnectToHost": "Не удалось подключиться к {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Введите корректный IP-адрес",
|
||||||
|
"enterValidTcpPort": "Введите корректный TCP-порт",
|
||||||
|
"ipAddress": "IP-адрес",
|
||||||
|
"tcpPort": "TCP-порт",
|
||||||
|
"customServerPort": "Пользовательский порт сервера",
|
||||||
|
"chat": "Чат",
|
||||||
|
"restoringPreviousLink": "Восстановление предыдущего подключения",
|
||||||
|
"noDeviceConnected": "Нет подключённого устройства",
|
||||||
|
"sendAdvert": "Отправить мой контакт",
|
||||||
|
"advertMode": "Отправить мой контакт",
|
||||||
|
"advertModeSubtitle": "Выберите, как далеко должны распространиться ваши контактные данные.",
|
||||||
|
"floodAdvertSent": "Контакт отправлен по всей mesh-сети",
|
||||||
|
"directAdvertSent": "Контакт отправлен ближайшим устройствам"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
"disconnect": "Prekini",
|
"disconnect": "Prekini",
|
||||||
"noDevicesFound": "Ni najdenih naprav",
|
"noDevicesFound": "Ni najdenih naprav",
|
||||||
"scanAgain": "Ponovi iskanje",
|
"scanAgain": "Ponovi iskanje",
|
||||||
"tapToConnect": "Tapnite za povezavo",
|
|
||||||
"deviceNotConnected": "Naprava ni povezana",
|
"deviceNotConnected": "Naprava ni povezana",
|
||||||
"locationPermissionDenied": "Dovoljenje za lokacijo zavrnjeno",
|
"locationPermissionDenied": "Dovoljenje za lokacijo zavrnjeno",
|
||||||
"locationPermissionPermanentlyDenied": "Dovoljenje za lokacijo trajno zavrnjeno. Prosimo, omogočite v Nastavitvah.",
|
"locationPermissionPermanentlyDenied": "Dovoljenje za lokacijo trajno zavrnjeno. Prosimo, omogočite v Nastavitvah.",
|
||||||
@@ -32,9 +31,7 @@
|
|||||||
"autoSystem": "Samodejno (Sistem)",
|
"autoSystem": "Samodejno (Sistem)",
|
||||||
"followSystemTheme": "Sledi sistemski temi",
|
"followSystemTheme": "Sledi sistemski temi",
|
||||||
"showRxTxIndicators": "Prikaži RX/TX kazalnike",
|
"showRxTxIndicators": "Prikaži RX/TX kazalnike",
|
||||||
"displayPacketActivity": "Prikaži kazalnike aktivnosti paketov v zgornji vrstici",
|
|
||||||
"disableMap": "Onemogoči zemljevid",
|
"disableMap": "Onemogoči zemljevid",
|
||||||
"disableMapDescription": "Skrij zavihek z zemljevidom za varčevanje z baterijo",
|
|
||||||
"language": "Jezik",
|
"language": "Jezik",
|
||||||
"chooseLanguage": "Izberite jezik",
|
"chooseLanguage": "Izberite jezik",
|
||||||
"save": "Shrani",
|
"save": "Shrani",
|
||||||
@@ -42,7 +39,6 @@
|
|||||||
"close": "Zapri",
|
"close": "Zapri",
|
||||||
"about": "O aplikaciji",
|
"about": "O aplikaciji",
|
||||||
"appVersion": "Različica aplikacije",
|
"appVersion": "Različica aplikacije",
|
||||||
"appName": "Ime aplikacije",
|
|
||||||
"aboutMeshCoreSar": "O MeshCore SAR",
|
"aboutMeshCoreSar": "O MeshCore SAR",
|
||||||
"aboutDescription": "Aplikacija za iskanje in reševanje, zasnovana za ekipe za odzivanje v nujnih primerih. Funkcije vključujejo:\n\n• BLE mesh omrežje za komunikacijo naprava-naprava\n• Brez povezave delujejo zemljevidi z več sloji\n• Sledenje članov ekipe v realnem času\n• SAR taktični označevalci (najdena oseba, ogenj, zbirališče)\n• Upravljanje stikov in sporočanje\n• GPS sledenje s kompasno smerjo\n• Predpomnenje ploščic zemljevida za uporabo brez povezave",
|
"aboutDescription": "Aplikacija za iskanje in reševanje, zasnovana za ekipe za odzivanje v nujnih primerih. Funkcije vključujejo:\n\n• BLE mesh omrežje za komunikacijo naprava-naprava\n• Brez povezave delujejo zemljevidi z več sloji\n• Sledenje članov ekipe v realnem času\n• SAR taktični označevalci (najdena oseba, ogenj, zbirališče)\n• Upravljanje stikov in sporočanje\n• GPS sledenje s kompasno smerjo\n• Predpomnenje ploščic zemljevida za uporabo brez povezave",
|
||||||
"technologiesUsed": "Uporabljene tehnologije:",
|
"technologiesUsed": "Uporabljene tehnologije:",
|
||||||
@@ -69,7 +65,6 @@
|
|||||||
"viewOnMap": "Poglej na zemljevidu",
|
"viewOnMap": "Poglej na zemljevidu",
|
||||||
"refresh": "Osveži",
|
"refresh": "Osveži",
|
||||||
"resetPath": "Ponastavi pot (preusmeri)",
|
"resetPath": "Ponastavi pot (preusmeri)",
|
||||||
"publicKeyCopied": "Javni ključ kopiran v odložišče",
|
|
||||||
"copiedToClipboard": "{label} kopirano v odložišče",
|
"copiedToClipboard": "{label} kopirano v odložišče",
|
||||||
"pleaseEnterPassword": "Prosimo, vnesite geslo",
|
"pleaseEnterPassword": "Prosimo, vnesite geslo",
|
||||||
"failedToSyncContacts": "Sinhronizacija stikov ni uspela: {error}",
|
"failedToSyncContacts": "Sinhronizacija stikov ni uspela: {error}",
|
||||||
@@ -148,15 +143,9 @@
|
|||||||
"type": "Vrsta",
|
"type": "Vrsta",
|
||||||
"publicKey": "Javni ključ",
|
"publicKey": "Javni ključ",
|
||||||
"lastSeen": "Nazadnje viden",
|
"lastSeen": "Nazadnje viden",
|
||||||
"roomStatus": "Status sobe",
|
|
||||||
"loginStatus": "Status prijave",
|
|
||||||
"notLoggedIn": "Ni prijavljen",
|
|
||||||
"adminAccess": "Administratorski dostop",
|
|
||||||
"yes": "Da",
|
"yes": "Da",
|
||||||
"no": "Ne",
|
"no": "Ne",
|
||||||
"permissions": "Dovoljenja",
|
"permissions": "Dovoljenja",
|
||||||
"passwordSaved": "Geslo shranjeno",
|
|
||||||
"locationColon": "Lokacija:",
|
|
||||||
"telemetry": "Telemetrija",
|
"telemetry": "Telemetrija",
|
||||||
"voltage": "Napetost",
|
"voltage": "Napetost",
|
||||||
"battery": "Baterija",
|
"battery": "Baterija",
|
||||||
@@ -164,8 +153,6 @@
|
|||||||
"humidity": "Vlažnost",
|
"humidity": "Vlažnost",
|
||||||
"pressure": "Tlak",
|
"pressure": "Tlak",
|
||||||
"gpsTelemetry": "GPS (Telemetrija)",
|
"gpsTelemetry": "GPS (Telemetrija)",
|
||||||
"updated": "Posodobljeno",
|
|
||||||
"pathResetInfo": "Pot ponastavljena za {name}. Naslednje sporočilo bo našlo novo pot.",
|
|
||||||
"reLoginToRoom": "Ponovna prijava v sobo",
|
"reLoginToRoom": "Ponovna prijava v sobo",
|
||||||
"heading": "Smer",
|
"heading": "Smer",
|
||||||
"elevation": "Nadmorska višina",
|
"elevation": "Nadmorska višina",
|
||||||
@@ -241,7 +228,6 @@
|
|||||||
"tapToViewOnMap": "Tapnite za prikaz na zemljevidu",
|
"tapToViewOnMap": "Tapnite za prikaz na zemljevidu",
|
||||||
"radioSettings": "Nastavitve radia",
|
"radioSettings": "Nastavitve radia",
|
||||||
"frequencyMHz": "Frekvenca (MHz)",
|
"frequencyMHz": "Frekvenca (MHz)",
|
||||||
"frequencyExample": "npr. 869.618",
|
|
||||||
"bandwidth": "Pasovna širina",
|
"bandwidth": "Pasovna širina",
|
||||||
"spreadingFactor": "Faktor razširitve",
|
"spreadingFactor": "Faktor razširitve",
|
||||||
"codingRate": "Razmerje kodiranja",
|
"codingRate": "Razmerje kodiranja",
|
||||||
@@ -264,38 +250,20 @@
|
|||||||
"noTrailRecorded": "Še ni posnete poti",
|
"noTrailRecorded": "Še ni posnete poti",
|
||||||
"startTrackingToRecord": "Začnite sledenje lokacije za snemanje poti",
|
"startTrackingToRecord": "Začnite sledenje lokacije za snemanje poti",
|
||||||
"trailControls": "Nadzor poti",
|
"trailControls": "Nadzor poti",
|
||||||
"contactTrails": "Poti stikov",
|
|
||||||
"showAllContactTrails": "Prikaži vse poti stikov",
|
|
||||||
"noContactsWithLocationHistory": "Ni stikov z zgodovino lokacije",
|
|
||||||
"showingTrailsForContacts": "Prikazujem poti za {count} stikov",
|
|
||||||
"individualContactTrails": "Posamezne poti stikov",
|
|
||||||
"deviceInformation": "Informacije o napravi",
|
|
||||||
"bleName": "BLE ime",
|
|
||||||
"meshName": "Mesh ime",
|
|
||||||
"notSet": "Ni nastavljeno",
|
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"version": "Različica",
|
"version": "Različica",
|
||||||
"buildDate": "Datum izdelave",
|
|
||||||
"firmware": "Vdelana programska oprema",
|
|
||||||
"maxContacts": "Maks. stikov",
|
"maxContacts": "Maks. stikov",
|
||||||
"maxChannels": "Maks. kanalov",
|
"maxChannels": "Maks. kanalov",
|
||||||
"publicInfo": "Javne informacije",
|
"publicInfo": "Javne informacije",
|
||||||
"meshNetworkName": "Ime mesh omrežja",
|
|
||||||
"nameBroadcastInMesh": "Ime, ki se oddaja v mesh oglasih",
|
|
||||||
"telemetryAndLocationSharing": "Telemetrija in deljenje lokacije",
|
|
||||||
"lat": "Šir",
|
|
||||||
"lon": "Dolž",
|
|
||||||
"useCurrentLocation": "Uporabi trenutno lokacijo",
|
"useCurrentLocation": "Uporabi trenutno lokacijo",
|
||||||
"noneUnknown": "Brez/Neznano",
|
"noneUnknown": "Brez/Neznano",
|
||||||
"chatNode": "Vozlišče za klepet",
|
"chatNode": "Vozlišče za klepet",
|
||||||
"repeater": "Ponavljalnik",
|
"repeater": "Ponavljalnik",
|
||||||
"roomChannel": "Soba/Kanal",
|
"roomChannel": "Soba/Kanal",
|
||||||
"typeNumber": "Tip {number}",
|
"typeNumber": "Tip {number}",
|
||||||
"copiedToClipboardShort": "Kopirano {label} v odložišče",
|
|
||||||
"failedToSave": "Shranjevanje ni uspelo: {error}",
|
"failedToSave": "Shranjevanje ni uspelo: {error}",
|
||||||
"failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}",
|
"failedToGetLocation": "Pridobivanje lokacije ni uspelo: {error}",
|
||||||
"sarTemplates": "SAR predloge",
|
"sarTemplates": "SAR predloge",
|
||||||
"manageSarTemplates": "Upravljanje SAR predlog",
|
|
||||||
"addTemplate": "Dodaj predlogo",
|
"addTemplate": "Dodaj predlogo",
|
||||||
"editTemplate": "Uredi predlogo",
|
"editTemplate": "Uredi predlogo",
|
||||||
"deleteTemplate": "Izbriši predlogo",
|
"deleteTemplate": "Izbriši predlogo",
|
||||||
@@ -323,7 +291,6 @@
|
|||||||
"noTemplates": "Ni razpoložljivih predlog",
|
"noTemplates": "Ni razpoložljivih predlog",
|
||||||
"tapAddToCreate": "Tapnite + za ustvarjanje prve predloge",
|
"tapAddToCreate": "Tapnite + za ustvarjanje prve predloge",
|
||||||
"ok": "V redu",
|
"ok": "V redu",
|
||||||
"permissionsSection": "Dovoljenja",
|
|
||||||
"locationPermission": "Dovoljenje za lokacijo",
|
"locationPermission": "Dovoljenje za lokacijo",
|
||||||
"checking": "Preverjanje...",
|
"checking": "Preverjanje...",
|
||||||
"locationPermissionGrantedAlways": "Odobreno (Vedno)",
|
"locationPermissionGrantedAlways": "Odobreno (Vedno)",
|
||||||
@@ -460,28 +427,6 @@
|
|||||||
"wizardNext": "Naprej",
|
"wizardNext": "Naprej",
|
||||||
"wizardGetStarted": "Začni",
|
"wizardGetStarted": "Začni",
|
||||||
"wizardWelcomeTitle": "Dobrodošli v MeshCore SAR",
|
"wizardWelcomeTitle": "Dobrodošli v MeshCore SAR",
|
||||||
"wizardWelcomeDescription": "Zmogljivo orodje za komunikacijo brez omrežja za reševalne operacije. S svojo ekipo se povežite z mesh radijsko tehnologijo, ko tradicionalna omrežja niso na voljo.",
|
|
||||||
"wizardConnectingTitle": "Povezava z radijem",
|
|
||||||
"wizardConnectingDescription": "Svoj telefon povežite z radijsko napravo MeshCore prek Bluetootha in začnite komunicirati brez omrežja.",
|
|
||||||
"wizardConnectingFeature1": "Poišče bližnje naprave MeshCore",
|
|
||||||
"wizardConnectingFeature2": "Seznanitev z radijsko napravo prek Bluetootha",
|
|
||||||
"wizardConnectingFeature3": "Deluje povsem brez povezave — internet ni potreben",
|
|
||||||
"wizardChannelTitle": "Kanali",
|
|
||||||
"wizardChannelDescription": "Pošiljajte sporočila vsem na kanalu — idealno za obvestila in koordinacijo ekipe.",
|
|
||||||
"wizardChannelFeature1": "Javni kanal za splošno komunikacijo ekipe",
|
|
||||||
"wizardChannelFeature2": "Ustvarite kanale po meri za določene skupine",
|
|
||||||
"wizardChannelFeature3": "Sporočila se samodejno posredujejo prek mreže",
|
|
||||||
"wizardContactsTitle": "Stiki",
|
|
||||||
"wizardContactsDescription": "Člani ekipe se prikažejo samodejno, ko se pridružijo mesh omrežju. Pošiljajte jim neposredna sporočila ali si oglejte njihovo lokacijo.",
|
|
||||||
"wizardContactsFeature1": "Samodejno odkrivanje stikov",
|
|
||||||
"wizardContactsFeature2": "Pošiljanje zasebnih neposrednih sporočil",
|
|
||||||
"wizardContactsFeature3": "Prikaz stanja baterije in časa zadnje aktivnosti",
|
|
||||||
"wizardMapTitle": "Zemljevid in lokacija",
|
|
||||||
"wizardMapDescription": "Spremljajte svojo ekipo v realnem času in označujte ključne lokacije za reševalne operacije.",
|
|
||||||
"wizardMapFeature1": "SAR označevalci za najdene osebe, požare in zbirna mesta",
|
|
||||||
"wizardMapFeature2": "Sledenje članom ekipe z GPS v realnem času",
|
|
||||||
"wizardMapFeature3": "Prenesite zemljevide za uporabo brez povezave",
|
|
||||||
"wizardMapFeature4": "Rišite oblike in delite taktične informacije",
|
|
||||||
"viewWelcomeTutorial": "Ogled vadnice dobrodošlice",
|
"viewWelcomeTutorial": "Ogled vadnice dobrodošlice",
|
||||||
"allTeamContacts": "Vsi stiki ekipe",
|
"allTeamContacts": "Vsi stiki ekipe",
|
||||||
"directMessagesInfo": "Neposredna sporočila s potrditvami. Poslano {count} članom ekipe.",
|
"directMessagesInfo": "Neposredna sporočila s potrditvami. Poslano {count} članom ekipe.",
|
||||||
@@ -511,8 +456,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 +508,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Aktivno",
|
||||||
|
"addContact": "Dodaj stik",
|
||||||
|
"all": "Vse",
|
||||||
|
"clearAllLabel": "Počisti vse",
|
||||||
|
"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",
|
||||||
|
"trace": "Sledenje",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"hideRepeatersOnMap": "Skrij posrednike na zemljevidu",
|
||||||
|
"setMapScale": "Nastavi merilo zemljevida",
|
||||||
|
"customMapScaleSaved": "Merilo zemljevida shranjeno",
|
||||||
|
"voiceBitrate": "Bitna hitrost glasu",
|
||||||
|
"voiceCompressor": "Kompresor glasu",
|
||||||
|
"voiceLimiter": "Omejevalnik glasu",
|
||||||
|
"micAutoGain": "Samodejna ojačitev mikrofona",
|
||||||
|
"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",
|
||||||
|
"busy": "Zasedeno",
|
||||||
|
"searchMessages": "Iskanje sporočil",
|
||||||
|
"sendImageFromGallery": "Pošlji sliko iz galerije",
|
||||||
|
"takePhoto": "Posnemi fotografijo",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Samodejno pošiljanje",
|
||||||
|
"autoSendDescription": "Pošlji po trenutni poti.",
|
||||||
|
"sendDirect": "Neposredno pošiljanje",
|
||||||
|
"sendDirectDescription": "Pošlji neposredno temu kontaktu.",
|
||||||
|
"sendFlood": "Pošiljanje s poplavo",
|
||||||
|
"sendFloodDescription": "Pošlji prek vseh repetitorjev.",
|
||||||
|
"postConnectDiscoveryTitle": "Odkriti repetitorje zdaj?",
|
||||||
|
"postConnectDiscoveryDescription": "Zaženite odkrivanje repetitorjev takoj po povezavi, da vidite bližnja vozlišča MeshCore in jih hitreje dodate v svoje omrežje.",
|
||||||
|
"aToZ": "A-Ž",
|
||||||
|
"add": "Dodaj",
|
||||||
|
"addAdditionalDetails": "Dodaj dodatne podrobnosti",
|
||||||
|
"addPeer": "Dodaj soležnika",
|
||||||
|
"addPeerManually": "Ročno dodaj soležnika",
|
||||||
|
"addedToContacts": "{name} dodan med stike",
|
||||||
|
"addedToSensors": "{name} dodan med senzorje",
|
||||||
|
"allowAll": "Dovoli vse",
|
||||||
|
"anonymousRxStats": "Anonimna RX statistika",
|
||||||
|
"appearance": "Videz",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Osnovna telemetrija",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Zvočni alarmi",
|
||||||
|
"cancelImageReceive": "Prekliči prejem slike",
|
||||||
|
"choose": "Izberi",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Izberite, katere zavihke in razdelke stikov prikazati",
|
||||||
|
"clearCache": "Počisti predpomnilnik",
|
||||||
|
"clearHistory": "Počisti zgodovino",
|
||||||
|
"clearLiveView": "Počisti pogled v živo",
|
||||||
|
"clearLogs": "Počisti dnevnike",
|
||||||
|
"clearOfflineCache": "Počisti predpomnilnik brez povezave?",
|
||||||
|
"clearOverlay": "Počisti prekrivanje",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Počisti pot šele po neuspehu vseh ponovitev",
|
||||||
|
"codecBitrateAndAudioProcessing": "Kodek, bitna hitrost in obdelava zvoka",
|
||||||
|
"connectByIpAddress": "Poveži prek IP naslova",
|
||||||
|
"controlWhichAlertsYouReceive": "Nadzirajte, katera opozorila prejemate",
|
||||||
|
"copyFullHex": "Kopiraj celoten Hex",
|
||||||
|
"copyRawResponse": "Kopiraj surov odgovor",
|
||||||
|
"couldNotLoadMetHistory": "Nalaganje MET zgodovine ni uspelo",
|
||||||
|
"customizeSensor": "Prilagodi {name}",
|
||||||
|
"data": "Podatki",
|
||||||
|
"deleteGroup": "Izbriši skupino",
|
||||||
|
"deleteStyleConfirm": "Izbriši {name}?",
|
||||||
|
"deleteThisDrawing": "Izbriši to {type}?",
|
||||||
|
"deny": "Zavrni",
|
||||||
|
"deviceInfo": "Informacije o napravi",
|
||||||
|
"deviceSettings": "Nastavitve naprave",
|
||||||
|
"deviceTimeSynced": "Čas naprave sinhroniziran s tem telefonom.",
|
||||||
|
"disableFastGpsPublishing": "Onemogoči hitro GPS objavljanje",
|
||||||
|
"displayMarkersAndTraceDatabase": "Prikaz, oznake in baza sledenja",
|
||||||
|
"download": "Prenesi",
|
||||||
|
"environmentalTelemetry": "Okoljska telemetrija",
|
||||||
|
"errorCheckingForUpdates": "Napaka pri preverjanju posodobitev: {error}",
|
||||||
|
"errorGeneric": "Napaka: {error}",
|
||||||
|
"errorOpeningDownload": "Napaka pri odpiranju prenosa: {error}",
|
||||||
|
"exportLogs": "Izvozi dnevnike",
|
||||||
|
"extraAggressiveAvifCompression": "Posebno agresivno AVIF stiskanje",
|
||||||
|
"failedToAddContact": "Dodajanje stika ni uspelo: {error}",
|
||||||
|
"failedToAddName": "Dodajanje {name} ni uspelo: {error}",
|
||||||
|
"failedToClearChannels": "Brisanje kanalov ni uspelo: {error}",
|
||||||
|
"failedToClearContacts": "Brisanje stikov ni uspelo: {error}",
|
||||||
|
"failedToConnectViaSerial": "Povezava prek serijske povezave ni uspela",
|
||||||
|
"failedToLoadPreviewImage": "Nalaganje predogleda slike ni uspelo: {error}",
|
||||||
|
"failedToLoadTrace": "Nalaganje sledi ni uspelo: {error}",
|
||||||
|
"failedToSendSarMarker": "Pošiljanje SAR oznake ni uspelo: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Nastavitev zvočnega alarma ni uspela: {error}",
|
||||||
|
"failedToSetGpsMode": "Nastavitev GPS načina ni uspela: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Sinhronizacija časa naprave ni uspela: {error}",
|
||||||
|
"failedToWipeDeviceData": "Brisanje podatkov naprave ni uspelo: {error}",
|
||||||
|
"fastGpsTargetChannel": "Ciljni kanal hitrega GPS-a",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Hitre posodobitve, pragovi in dovoljenja",
|
||||||
|
"fetchingCatalog": "Pridobivanje kataloga...",
|
||||||
|
"fifteenMinutes": "15 minut",
|
||||||
|
"fifteenSeconds": "15 sekund",
|
||||||
|
"filterByDirection": "Filtriraj po smeri",
|
||||||
|
"finish": "Končaj",
|
||||||
|
"fiveMinutes": "5 minut",
|
||||||
|
"fiveSeconds": "5 sekund",
|
||||||
|
"fix": "Popravi",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Politika GPS oglašanja",
|
||||||
|
"gpsAndLocation": "GPS in lokacija",
|
||||||
|
"gpsDiagnostics": "GPS diagnostika",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Pomoč",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Skrito",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Kako radio samodejno dodaja odkrita vozlišča.",
|
||||||
|
"images": "Slike",
|
||||||
|
"importProfile": "Uvozi profil",
|
||||||
|
"lastFix": "Zadnja posneta lokacija",
|
||||||
|
"latest": "Najnovejše",
|
||||||
|
"latitude": "Geografska širina",
|
||||||
|
"loadASavedRegion": "Naloži shranjeno regijo",
|
||||||
|
"loading": "Nalaganje",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Deljenje lokacije, strojna oprema in interval posodobitev.",
|
||||||
|
"locationTelemetry": "Telemetrija lokacije",
|
||||||
|
"lockDestination": "Zakleni cilj",
|
||||||
|
"longitude": "Geografska dolžina",
|
||||||
|
"mapStyle": "Slog zemljevida",
|
||||||
|
"max": "Maks",
|
||||||
|
"maxZoom": "Maks. povečava",
|
||||||
|
"messaging": "Sporočanje",
|
||||||
|
"metHistory": "MET zgodovina",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Min. povečava",
|
||||||
|
"moveDown": "Premakni navzdol",
|
||||||
|
"moveUp": "Premakni navzgor",
|
||||||
|
"multiAckMode": "Način večkratne ACK potrditve",
|
||||||
|
"multiDeviceWorkspaceManagement": "Upravljanje delovnega prostora z več napravami",
|
||||||
|
"myLocation": "Moja lokacija",
|
||||||
|
"nameAndTelemetryShared": "Ime in telemetrija deljena z drugimi napravami.",
|
||||||
|
"newVersionAvailable": "Nova različica: {version}",
|
||||||
|
"newer": "Novejše",
|
||||||
|
"noNeighboursFound": "Ni najdenih sosedov",
|
||||||
|
"noServersFound": "Ni najdenih strežnikov",
|
||||||
|
"notSet": "Ni nastavljeno",
|
||||||
|
"notifications": "Obvestila",
|
||||||
|
"offZeroSeconds": "Izključeno (0 s)",
|
||||||
|
"offlineMaps": "Zemljevidi brez povezave",
|
||||||
|
"older": "Starejše",
|
||||||
|
"oneByteMode0": "1 bajt (način 0)",
|
||||||
|
"oneHour": "1 ura",
|
||||||
|
"oneMinute": "1 minuta",
|
||||||
|
"openPacketLogs": "Odpri dnevnike paketov",
|
||||||
|
"packetTypeHelp": "Pomoč za vrsto paketa",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Prilepite deljeno povezavo ali Hex oglas",
|
||||||
|
"pathHash": "Zgoščevalna vrednost poti",
|
||||||
|
"pathSize": "Velikost poti",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Poligon",
|
||||||
|
"rawResponseCopied": "Surov odgovor kopiran",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Ponovno pošlji prek najbližjega posrednika ob neuspehu",
|
||||||
|
"resolveContact": "Razreši stik",
|
||||||
|
"routingRetriesAndDestinationLock": "Usmerjanje, ponovitve in zaklepanje cilja",
|
||||||
|
"samples": "Vzorci",
|
||||||
|
"satellites": "Sateliti",
|
||||||
|
"saving": "Shranjujem...",
|
||||||
|
"search": "Iskanje",
|
||||||
|
"searchByNameOrToken": "Iskanje po imenu ali žetonu",
|
||||||
|
"searchDiscoveredNodes": "Iskanje odkritih vozlišč",
|
||||||
|
"searchInCurrentFilter": "Iskanje v trenutnem filtru",
|
||||||
|
"searchLogs": "Iskanje dnevnikov...",
|
||||||
|
"searchSensors": "Iskanje senzorjev",
|
||||||
|
"searchThisSection": "Iskanje v tem razdelku",
|
||||||
|
"selectedRelays": "Izbrani posredniki",
|
||||||
|
"sensor": "Senzor",
|
||||||
|
"serialError": "Napaka serijske povezave: {error}",
|
||||||
|
"setPathForContact": "Nastavi pot za {name}",
|
||||||
|
"shareLiveGps": "Deli GPS v živo",
|
||||||
|
"shareMyTiles": "Deli moje ploščice",
|
||||||
|
"sharingLocationFromPhone": "Deljenje lokacije na tem kanalu s telefona.",
|
||||||
|
"sharingLocationFromRadio": "Deljenje lokacije na tem kanalu z radia.",
|
||||||
|
"signalDbm": "Signal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Velikost, stiskanje in predogled",
|
||||||
|
"skip": "Preskoči",
|
||||||
|
"smallerFileSize": "Manjša velikost datoteke",
|
||||||
|
"sort": "Razvrsti",
|
||||||
|
"stoppedSharingLocation": "Deljenje lokacije na tem kanalu ustavljeno.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Zadrži obvestila, ko je aplikacija v ospredju",
|
||||||
|
"tabsAndNavigation": "Zavihki in navigacija",
|
||||||
|
"templatesAndTutorials": "Predloge in vadnice",
|
||||||
|
"tenMinutes": "10 minut",
|
||||||
|
"tenSeconds": "10 sekund",
|
||||||
|
"testSendUpdate": "Preizkusno pošlji posodobitev",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, jezik in nastavitve prikaza",
|
||||||
|
"thirtyMinutes": "30 minut",
|
||||||
|
"thirtySeconds": "30 sekund",
|
||||||
|
"threeBytesMode2": "3 bajti (način 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Statistika prometa, zgodovina sporočil in razvojna orodja",
|
||||||
|
"twoBytesMode1": "2 bajta (način 1)",
|
||||||
|
"twoMinutes": "2 minuti",
|
||||||
|
"undo": "Razveljavi",
|
||||||
|
"updated": "Posodobljeno",
|
||||||
|
"useContactFlags": "Uporabi zastavice stikov",
|
||||||
|
"useSavedCoordinates": "Uporabi shranjene koordinate",
|
||||||
|
"view": "Pogled",
|
||||||
|
"viewNeighbours": "Poglej sosede",
|
||||||
|
"viewPublicStats": "Poglej javno statistiko",
|
||||||
|
"setRegionScope": "Nastavi obseg regije",
|
||||||
|
"regionScope": "Obseg regije",
|
||||||
|
"regionScopeNone": "Brez (globalno)",
|
||||||
|
"clearRegionScope": "Počisti obseg",
|
||||||
|
"regionScopeWarning": "Samo repetitorji, ki dovoljujejo to regijo, bodo posredovali.",
|
||||||
|
"discoverRegions": "Odkrij iz repetitorjev",
|
||||||
|
"discoveringRegions": "Odkrivanje regij...",
|
||||||
|
"enterRegionName": "Ime regije (npr. auckland)",
|
||||||
|
"noRegionsFound": "Na tem repetitorju ni najdenih regij.",
|
||||||
|
"regionScopeSet": "Obseg regije nastavljen na {name}",
|
||||||
|
"regionScopeCleared": "Obseg regije počiščen",
|
||||||
|
"simpleMode": "Enostavni način",
|
||||||
|
"simpleModeDescription": "Samo klepet in zemljevid s priljubljenimi stiki",
|
||||||
|
"simpleModeRequiresLockedDestination": "Za uporabo enostavnega načina najprej omogočite »Zakleni cilj« s kanalom",
|
||||||
|
"noFavouriteContacts": "Ni priljubljenih stikov",
|
||||||
|
"selectDestinationFirst": "Najprej izberite kanal, stik ali sobo",
|
||||||
|
"recipientNoLongerAvailable": "Prejemnik ni več na voljo",
|
||||||
|
"failedToSendMessage": "Pošiljanje ni uspelo: {error}",
|
||||||
|
"tictactoeDirectOnly": "Križci in krožci delujejo samo v neposrednih sporočilih. Najprej izberite stik.",
|
||||||
|
"deviceKeyUnavailable": "Ključ naprave ni na voljo",
|
||||||
|
"imageCompressionFailed": "Stiskanje slike ni uspelo",
|
||||||
|
"imageFragmentationFailed": "Razdelitev slike ni uspela",
|
||||||
|
"failedToAnnounceImage": "Najava slike ni uspela",
|
||||||
|
"noRecipientSelected": "Prejemnik ni izbran",
|
||||||
|
"imageSendFailed": "Pošiljanje slike ni uspelo",
|
||||||
|
"microphonePermissionRequiredForVoice": "Za glasovna sporočila je potrebno dovoljenje za mikrofon",
|
||||||
|
"recordingDiscarded": "Posnetek je zavržen",
|
||||||
|
"publicChannelMediaWarning": "Pravkar boste poslali {mediaType} v javni kanal. To ni priporočljivo, ker lahko vsebino prejmejo vsi v omrežju mesh. Izberite zaseben ali označen kanal, razen če to res želite.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Za deljenje lokacije izberite zaseben kanal.",
|
||||||
|
"stopSharingMyLocation": "Prenehaj deliti mojo lokacijo",
|
||||||
|
"shareMyLocation": "Deli mojo lokacijo",
|
||||||
|
"stopRecording": "Ustavi snemanje",
|
||||||
|
"recordVoice": "Posnemi glasovno sporočilo",
|
||||||
|
"moreActions": "Več dejanj",
|
||||||
|
"noMessagesInFilter": "V tem filtru ni sporočil",
|
||||||
|
"noMatchesInFilter": "V tem filtru ni zadetkov",
|
||||||
|
"syncFailed": "Sinhronizacija ni uspela: {error}",
|
||||||
|
"invalidLink": "Neveljavna povezava",
|
||||||
|
"cannotOpenLink": "Povezave ni mogoče odpreti",
|
||||||
|
"failedToOpenLink": "Odpiranje povezave ni uspelo",
|
||||||
|
"failedToResendMessage": "Ponovno pošiljanje sporočila ni uspelo",
|
||||||
|
"retryFailed": "Ponovni poskus ni uspel: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Izbriši risbo in sporočilo",
|
||||||
|
"lastEchoRelay": "Posrednik zadnjega odmeva",
|
||||||
|
"lastEchoPath": "Pot zadnjega odmeva",
|
||||||
|
"lastEchoBytesReport": "Poročilo o bajtih zadnjega odmeva",
|
||||||
|
"noGpsCoordinatesAvailable": "GPS-koordinate niso na voljo",
|
||||||
|
"notASarMarker": "To ni SAR označevalec",
|
||||||
|
"failedToSaveTemplate": "Shranjevanje predloge ni uspelo: {error}",
|
||||||
|
"drawingNotFound": "Risbe ni mogoče najti",
|
||||||
|
"unknownDrawingType": "Neznana vrsta risbe",
|
||||||
|
"customMapMarker": "Označevalec karte po meri",
|
||||||
|
"customMapPointLabel": "Točka: {point}",
|
||||||
|
"mapIdLabel": "ID karte: {id}",
|
||||||
|
"cannotFetchVoice": "Glasovnega sporočila ni mogoče prenesti",
|
||||||
|
"senderContactUnknown": "Stik pošiljatelja ni znan. Najprej sinhronizirajte stike.",
|
||||||
|
"senderRouteUnknown": "Pot pošiljatelja ni znana. Najprej sinhronizirajte stike/poti.",
|
||||||
|
"messageTooFar": "Sporočilo je predaleč ({hops} skokov, največ {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Pot pošiljatelja se ni odzvala na preverjanje poti. Sinhronizirajte stike/poti in poskusite znova.",
|
||||||
|
"senderRouteNoRawResponse": "Pot pošiljatelja se ni odzvala na neposredni prenosni poti.",
|
||||||
|
"voiceFetchOverHops": "Prenos glasu prek {hops} skokov lahko traja dlje.",
|
||||||
|
"voiceReceiveCanceled": "Sprejem glasovnega sporočila je preklican",
|
||||||
|
"fetchingMissingVoiceFragments": "Pridobivanje manjkajočih glasovnih delov",
|
||||||
|
"receivingVoice": "Sprejemanje glasu",
|
||||||
|
"cannotFetchImage": "Slike ni mogoče prenesti",
|
||||||
|
"imageFetchOverHops": "Prenos slike prek {hops} skokov lahko traja dlje.",
|
||||||
|
"imageAlreadyBeingReceived": "Slika se že sprejema",
|
||||||
|
"loadImage": "Naloži sliko",
|
||||||
|
"imageFetchFailedToSendRequest": "Zahteve za prenos slike ni bilo mogoče poslati",
|
||||||
|
"imageUnavailable": "Slika trenutno ni na voljo",
|
||||||
|
"imageFetchTimedOut": "Prenos slike je potekel",
|
||||||
|
"imageReceiveCanceled": "Sprejem slike je preklican",
|
||||||
|
"closeImagePreview": "Zapri predogled slike",
|
||||||
|
"recordingReleaseToSend": "Snemanje ... spustite za pošiljanje glasu",
|
||||||
|
"sendingVoice": "Pošiljanje glasu ...",
|
||||||
|
"sendLongPressToRecordVoice": "Pošlji (dolg pritisk za snemanje glasu)",
|
||||||
|
"discardRecording": "Zavrzi posnetek",
|
||||||
|
"hopCount": "{count, plural, one{{count} skok} two{{count} skoka} few{{count} skoki} other{{count} skokov}}",
|
||||||
|
"channelsSectionDescription": "Oddajni kanali za bližnji promet v omrežju mesh",
|
||||||
|
"roomsSectionDescription": "Skupni prostori za stalno usklajevanje ekipe",
|
||||||
|
"contactsSectionDescription": "Osebe in naprave, ki jih lahko dosežete neposredno",
|
||||||
|
"noRecentChatters": "Ni nedavnih klepetalcev",
|
||||||
|
"public": "Javno",
|
||||||
|
"failedToSendTicTacToeMove": "Poteze pri križcih in krožcih ni bilo mogoče poslati",
|
||||||
|
"youWon": "Zmagali ste",
|
||||||
|
"opponentWon": "Zmagal je nasprotnik",
|
||||||
|
"gameDraw": "Neodločeno",
|
||||||
|
"yourTurn": "Vi ste na vrsti",
|
||||||
|
"opponentTurn": "Na vrsti je nasprotnik",
|
||||||
|
"pleaseEnterValidCoordinates": "Vnesite veljavne koordinate",
|
||||||
|
"locationNotAvailable": "Lokacija ni na voljo",
|
||||||
|
"customCaveMapPoint": "Točka jamske karte po meri",
|
||||||
|
"mapPoint": "Točka na karti",
|
||||||
|
"fetchingMissingImageFragments": "Pridobivanje manjkajočih delov …",
|
||||||
|
"loadingImage": "Nalaganje …",
|
||||||
|
"receivingImage": "Sprejemanje …",
|
||||||
|
"tapToLoad": "Tapnite za nalaganje",
|
||||||
|
"connectDevice": "Poveži napravo",
|
||||||
|
"chooseTransportSubtitle": "Izberite način povezave: Bluetooth, WiFi ali serijska",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Omrežje",
|
||||||
|
"serial": "Serijsko",
|
||||||
|
"scan": "Poišči",
|
||||||
|
"pressScanToSearchForDevices": "Pritisnite Poišči za iskanje naprav v bližini",
|
||||||
|
"failedToConnectToDevice": "Povezava z napravo {name} ni uspela",
|
||||||
|
"unknownDevice": "Neznana naprava",
|
||||||
|
"showingCachedResultsTapRefresh": "Prikazani so predpomnjeni rezultati. Za ponovno iskanje tapnite osveži.",
|
||||||
|
"scanningLocalNetworkOnPort": "Iskanje naprav MeshCore WiFi v lokalnem omrežju na vratih {port}",
|
||||||
|
"cancelScanAndAddServer": "Prekliči iskanje in dodaj strežnik",
|
||||||
|
"addServer": "Dodaj strežnik",
|
||||||
|
"scanningProgressIps": "Iskanje … {scanned}/{total} IP naslovov",
|
||||||
|
"cancelAndEnterManually": "Prekliči in vnesi ročno",
|
||||||
|
"noRecentOrDiscoveredServers": "Še ni nedavnih ali odkritih strežnikov",
|
||||||
|
"recentlyUsed": "Nedavno uporabljeno",
|
||||||
|
"connecting": "Povezovanje …",
|
||||||
|
"discoveredOnThisNetwork": "Odkrito v tem omrežju",
|
||||||
|
"serverNoLongerAvailable": "Strežnik na {host}:{port} ni več dosegljiv. Ponovite iskanje, da najdete aktivne strežnike.",
|
||||||
|
"failedToConnectToHost": "Povezava s strežnikom {host}:{port} ni uspela",
|
||||||
|
"enterValidIpAddress": "Vnesite veljaven IP naslov",
|
||||||
|
"enterValidTcpPort": "Vnesite veljavna vrata TCP",
|
||||||
|
"ipAddress": "IP naslov",
|
||||||
|
"tcpPort": "Vrata TCP",
|
||||||
|
"customServerPort": "Vrata strežnika po meri",
|
||||||
|
"chat": "Klepet",
|
||||||
|
"restoringPreviousLink": "Obnavljanje prejšnje povezave",
|
||||||
|
"noDeviceConnected": "Ni povezane naprave",
|
||||||
|
"sendAdvert": "Pošlji moj kontakt",
|
||||||
|
"advertMode": "Pošlji moj kontakt",
|
||||||
|
"advertModeSubtitle": "Izberite, kako daleč naj potujejo vaši kontaktni podatki.",
|
||||||
|
"floodAdvertSent": "Kontakt poslan z razprševanjem po omrežju",
|
||||||
|
"directAdvertSent": "Kontakt poslan neposredno bližnjim napravam"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Button to restart BLE scanning"
|
"description": "Button to restart BLE scanning"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Bağlanmak için dokunun",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Subtitle text for device in scan list"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Cihaz bağlı değil",
|
"deviceNotConnected": "Cihaz bağlı değil",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Error message when device is not connected"
|
"description": "Error message when device is not connected"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Setting to show RX/TX indicators"
|
"description": "Setting to show RX/TX indicators"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Üst çubukta paket etkinliği göstergelerini göster",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description for RX/TX indicators setting"
|
|
||||||
},
|
|
||||||
"disableMap": "Haritayı devre dışı bırak",
|
"disableMap": "Haritayı devre dışı bırak",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Setting to disable the map tab"
|
"description": "Setting to disable the map tab"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Pil kullanımını azaltmak için harita sekmesini gizle",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description for disable map setting"
|
|
||||||
},
|
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Language setting label"
|
"description": "Language setting label"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "App version label"
|
"description": "App version label"
|
||||||
},
|
},
|
||||||
"appName": "Uygulama adı",
|
|
||||||
"@appName": {
|
|
||||||
"description": "App name label"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "MeshCore SAR hakkında",
|
"aboutMeshCoreSar": "MeshCore SAR hakkında",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "About dialog title"
|
"description": "About dialog title"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action to reset contact path for re-routing"
|
"description": "Action to reset contact path for re-routing"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Genel anahtar panoya kopyalandı",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Success message when public key is copied"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} panoya kopyalandı",
|
"copiedToClipboard": "{label} panoya kopyalandı",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Success message when a value is copied to clipboard",
|
"description": "Success message when a value is copied to clipboard",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Last seen label"
|
"description": "Last seen label"
|
||||||
},
|
},
|
||||||
"roomStatus": "Oda durumu",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Room status section header"
|
|
||||||
},
|
|
||||||
"loginStatus": "Giriş durumu",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Login status label"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Giriş yapılmadı",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Not logged in status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Yönetici erişimi",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Admin access label"
|
|
||||||
},
|
|
||||||
"yes": "Evet",
|
"yes": "Evet",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Yes answer"
|
"description": "Yes answer"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Permissions label"
|
"description": "Permissions label"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Parola kaydedildi",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Password saved label"
|
|
||||||
},
|
|
||||||
"locationColon": "Konum:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Location section header"
|
|
||||||
},
|
|
||||||
"telemetry": "Telemetri",
|
"telemetry": "Telemetri",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Telemetry section header"
|
"description": "Telemetry section header"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS from telemetry label"
|
"description": "GPS from telemetry label"
|
||||||
},
|
},
|
||||||
"updated": "Güncellendi",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Updated timestamp label"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "{name} için yol sıfırlandı. Sonraki mesaj yeni bir rota bulacak.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Info message after path reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Odaya yeniden giriş yap",
|
"reLoginToRoom": "Odaya yeniden giriş yap",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Button to re-login to room"
|
"description": "Button to re-login to room"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Label for radio frequency field"
|
"description": "Label for radio frequency field"
|
||||||
},
|
},
|
||||||
"frequencyExample": "örn. 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Helper text example for frequency"
|
|
||||||
},
|
|
||||||
"bandwidth": "Bant genişliği",
|
"bandwidth": "Bant genişliği",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Label for bandwidth dropdown"
|
"description": "Label for bandwidth dropdown"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
"contactTrails": "Kişi izleri",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Contact trails section header"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Tüm kişi izlerini göster",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Toggle label to show all contact trails"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Konum geçmişi olan kişi yok",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtitle when no contacts have trails"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "{count} kişi için izler gösteriliyor",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtitle showing number of contacts with trails",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Tek tek kişi izleri",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Expansion tile title for individual contact trails"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Cihaz bilgileri",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Device information section header"
|
|
||||||
},
|
|
||||||
"bleName": "BLE adı",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Bluetooth Low Energy device name label"
|
|
||||||
},
|
|
||||||
"meshName": "Mesh adı",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh network name label"
|
|
||||||
},
|
|
||||||
"notSet": "Ayarlanmadı",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Label when a value is not set"
|
|
||||||
},
|
|
||||||
"model": "Model",
|
"model": "Model",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Device model label"
|
"description": "Device model label"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Version label"
|
"description": "Version label"
|
||||||
},
|
},
|
||||||
"buildDate": "Derleme tarihi",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Firmware build date label"
|
|
||||||
},
|
|
||||||
"firmware": "Bellenim",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Firmware label"
|
|
||||||
},
|
|
||||||
"maxContacts": "Maks kişi",
|
"maxContacts": "Maks kişi",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Maximum contacts capacity label"
|
"description": "Maximum contacts capacity label"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Public information section header"
|
"description": "Public information section header"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Mesh ağ adı",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh network name field label"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Mesh duyurularında yayınlanan ad",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Helper text for mesh network name field"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Telemetri ve konum paylaşımı",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Telemetry and location sharing toggle label"
|
|
||||||
},
|
|
||||||
"lat": "Enl.",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Latitude field label (short form)"
|
|
||||||
},
|
|
||||||
"lon": "Boyl.",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Longitude field label (short form)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Geçerli konumu kullan",
|
"useCurrentLocation": "Geçerli konumu kullan",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip for use current location button"
|
"description": "Tooltip for use current location button"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} panoya kopyalandı",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Short success message when copying to clipboard",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Kaydetme başarısız: {error}",
|
"failedToSave": "Kaydetme başarısız: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Generic error message for save failures",
|
"description": "Generic error message for save failures",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "SAR templates menu title"
|
"description": "SAR templates menu title"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "SAR şablonlarını yönet",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "Subtitle for SAR templates settings"
|
|
||||||
},
|
|
||||||
"addTemplate": "Şablon ekle",
|
"addTemplate": "Şablon ekle",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "Button to add new SAR template"
|
"description": "Button to add new SAR template"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "OK button label"
|
"description": "OK button label"
|
||||||
},
|
},
|
||||||
"permissionsSection": "İzinler",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "Permissions section header"
|
|
||||||
},
|
|
||||||
"locationPermission": "Konum izni",
|
"locationPermission": "Konum izni",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "Location permission label"
|
"description": "Location permission label"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "Welcome wizard first page title"
|
"description": "Welcome wizard first page title"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "Arama ve kurtarma operasyonları için güçlü bir şebeke dışı iletişim aracı. Geleneksel ağlar kullanılamadığında ekibinizle mesh radyo teknolojisi üzerinden bağlantı kurun.",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "Welcome wizard first page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "Radyonuza bağlanma",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "Wizard connecting page title"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "Şebeke dışı iletişime başlamak için akıllı telefonunuzu Bluetooth üzerinden bir MeshCore radyo cihazına bağlayın.",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "Wizard connecting page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "Yakındaki MeshCore cihazlarını tarayın",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "Wizard connecting feature 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "Radyonuzla Bluetooth üzerinden eşleşin",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "Wizard connecting feature 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "Tamamen çevrimdışı çalışır - internet gerekmez",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "Wizard connecting feature 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "Kanallar",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "Wizard channel page title"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "Bir kanaldaki herkese mesaj yayınlayın; ekip genelindeki duyurular ve koordinasyon için idealdir.",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "Wizard channel page description"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "Genel ekip iletişimi için herkese açık kanal",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "Wizard channel feature 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "Belirli gruplar için özel kanallar oluşturun",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "Wizard channel feature 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "Mesajlar mesh tarafından otomatik iletilir",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "Wizard channel feature 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "Kişiler",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "Wizard contacts page title"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "Ekip üyeleriniz mesh ağına katıldıkça otomatik olarak görünür. Onlara doğrudan mesaj gönderin veya konumlarını görüntüleyin.",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "Wizard contacts page description"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "Kişiler otomatik bulunur",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "Wizard contacts feature 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "Özel doğrudan mesaj gönderin",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "Wizard contacts feature 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "Pil seviyesini ve son görülme zamanını görüntüleyin",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "Wizard contacts feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "Harita ve konum",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "Wizard map page title"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "Ekibinizi gerçek zamanlı izleyin ve arama kurtarma operasyonları için önemli konumları işaretleyin.",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "Wizard map page description"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "Bulunan kişiler, yangınlar ve toplanma alanları için SAR işaretleri",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "Wizard map feature 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "Ekip üyelerinin gerçek zamanlı GPS takibi",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "Wizard map feature 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "Uzak bölgeler için çevrimdışı haritalar indirin",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "Wizard map feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "Şekiller çizin ve taktik bilgileri paylaşın",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "Wizard map feature 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "Karşılama eğitimini görüntüle",
|
"viewWelcomeTutorial": "Karşılama eğitimini görüntüle",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "Settings option to re-show welcome wizard"
|
"description": "Settings option to re-show welcome wizard"
|
||||||
@@ -2328,5 +2093,525 @@
|
|||||||
"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",
|
||||||
|
"active": "Aktif",
|
||||||
|
"addContact": "Kişi ekle",
|
||||||
|
"all": "Tümü",
|
||||||
|
"clearAllLabel": "Tümünü 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ş",
|
||||||
|
"trace": "İz",
|
||||||
|
"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ı",
|
||||||
|
"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",
|
||||||
|
"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",
|
||||||
|
"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ı",
|
||||||
|
"voiceLimiter": "Ses sınırlayıcı",
|
||||||
|
"micAutoGain": "Mikrofon otomatik kazancı",
|
||||||
|
"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",
|
||||||
|
"busy": "Meşgul",
|
||||||
|
"searchMessages": "Mesajlarda ara",
|
||||||
|
"sendImageFromGallery": "Galeriden resim gönder",
|
||||||
|
"takePhoto": "Fotoğraf çek",
|
||||||
|
"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.",
|
||||||
|
"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",
|
||||||
|
"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.",
|
||||||
|
"autoSend": "Otomatik Gönder",
|
||||||
|
"autoSendDescription": "Mevcut yol üzerinden gönder.",
|
||||||
|
"sendDirect": "Doğrudan Gönder",
|
||||||
|
"sendDirectDescription": "Bu kişiye doğrudan gönder.",
|
||||||
|
"sendFlood": "Taşkın Gönder",
|
||||||
|
"sendFloodDescription": "Tüm aktarıcılar üzerinden gönder.",
|
||||||
|
"postConnectDiscoveryTitle": "Aktarıcıları şimdi keşfet?",
|
||||||
|
"postConnectDiscoveryDescription": "Bağlandıktan hemen sonra aktarıcı keşfi çalıştırarak yakındaki MeshCore düğümlerini görün ve ağınıza daha hızlı ekleyin.",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "Ekle",
|
||||||
|
"addAdditionalDetails": "Ek ayrıntılar ekle",
|
||||||
|
"addPeer": "Eş ekle",
|
||||||
|
"addPeerManually": "Eşi elle ekle",
|
||||||
|
"addedToContacts": "{name} kişilere eklendi",
|
||||||
|
"addedToSensors": "{name} Sensörlere eklendi",
|
||||||
|
"allowAll": "Tümüne izin ver",
|
||||||
|
"anonymousRxStats": "Anonim RX istatistikleri",
|
||||||
|
"appearance": "Görünüm",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Temel telemetri",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Buzzer uyarıları",
|
||||||
|
"cancelImageReceive": "Görüntü alımını iptal et",
|
||||||
|
"choose": "Seç",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Gösterilecek sekmeleri ve kişi bölümlerini seçin",
|
||||||
|
"clearCache": "Önbelleği temizle",
|
||||||
|
"clearHistory": "Geçmişi temizle",
|
||||||
|
"clearLiveView": "Canlı görünümü temizle",
|
||||||
|
"clearLogs": "Günlükleri temizle",
|
||||||
|
"clearOfflineCache": "Çevrimdışı önbellek temizlensin mi?",
|
||||||
|
"clearOverlay": "Katmanı temizle",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Rotayı yalnızca tüm yeniden denemeler başarısız olduktan sonra temizle",
|
||||||
|
"codecBitrateAndAudioProcessing": "Codec, bit hızı ve ses işleme",
|
||||||
|
"connectByIpAddress": "IP adresiyle bağlan",
|
||||||
|
"controlWhichAlertsYouReceive": "Hangi uyarıları alacağınızı kontrol edin",
|
||||||
|
"copyFullHex": "Tam Hex kopyala",
|
||||||
|
"copyRawResponse": "Ham yanıtı kopyala",
|
||||||
|
"couldNotLoadMetHistory": "MET geçmişi yüklenemedi",
|
||||||
|
"customizeSensor": "{name} sensörünü özelleştir",
|
||||||
|
"data": "Veri",
|
||||||
|
"deleteGroup": "Grubu sil",
|
||||||
|
"deleteStyleConfirm": "{name} silinsin mi?",
|
||||||
|
"deleteThisDrawing": "Bu {type} silinsin mi?",
|
||||||
|
"deny": "Reddet",
|
||||||
|
"deviceInfo": "Cihaz bilgisi",
|
||||||
|
"deviceSettings": "Cihaz Ayarları",
|
||||||
|
"deviceTimeSynced": "Cihaz saati bu telefonla senkronize edildi.",
|
||||||
|
"disableFastGpsPublishing": "Hızlı GPS yayınını devre dışı bırak",
|
||||||
|
"displayMarkersAndTraceDatabase": "Ekran, işaretçiler ve iz veritabanı",
|
||||||
|
"download": "İndir",
|
||||||
|
"environmentalTelemetry": "Çevresel telemetri",
|
||||||
|
"errorCheckingForUpdates": "Güncelleme kontrolünde hata: {error}",
|
||||||
|
"errorGeneric": "Hata: {error}",
|
||||||
|
"errorOpeningDownload": "İndirme açılırken hata: {error}",
|
||||||
|
"exportLogs": "Günlükleri dışa aktar",
|
||||||
|
"extraAggressiveAvifCompression": "Ekstra agresif AVIF sıkıştırma",
|
||||||
|
"failedToAddContact": "Kişi eklenemedi: {error}",
|
||||||
|
"failedToAddName": "{name} eklenemedi: {error}",
|
||||||
|
"failedToClearChannels": "Kanallar temizlenemedi: {error}",
|
||||||
|
"failedToClearContacts": "Kişiler temizlenemedi: {error}",
|
||||||
|
"failedToConnectViaSerial": "Seri bağlantı kurulamadı",
|
||||||
|
"failedToLoadPreviewImage": "Önizleme görüntüsü yüklenemedi: {error}",
|
||||||
|
"failedToLoadTrace": "İz yüklenemedi: {error}",
|
||||||
|
"failedToSendSarMarker": "SAR işaretçisi gönderilemedi: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Buzzer modu ayarlanamadı: {error}",
|
||||||
|
"failedToSetGpsMode": "GPS modu ayarlanamadı: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Cihaz saati senkronize edilemedi: {error}",
|
||||||
|
"failedToWipeDeviceData": "Cihaz verileri silinemedi: {error}",
|
||||||
|
"fastGpsTargetChannel": "Hızlı GPS hedef kanalı",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Hızlı güncellemeler, eşikler ve izinler",
|
||||||
|
"fetchingCatalog": "Katalog getiriliyor...",
|
||||||
|
"fifteenMinutes": "15 dakika",
|
||||||
|
"fifteenSeconds": "15 saniye",
|
||||||
|
"filterByDirection": "Yöne göre filtrele",
|
||||||
|
"finish": "Bitir",
|
||||||
|
"fiveMinutes": "5 dakika",
|
||||||
|
"fiveSeconds": "5 saniye",
|
||||||
|
"fix": "Düzelt",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "GPS ilan politikası",
|
||||||
|
"gpsAndLocation": "GPS ve Konum",
|
||||||
|
"gpsDiagnostics": "GPS tanılama",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Yardım",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Gizli",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Radyonun keşfedilen düğümleri otomatik olarak nasıl eklediği.",
|
||||||
|
"images": "Görüntüler",
|
||||||
|
"importProfile": "Profili içe aktar",
|
||||||
|
"lastFix": "Son konum",
|
||||||
|
"latest": "En yeni",
|
||||||
|
"latitude": "Enlem",
|
||||||
|
"loadASavedRegion": "Kayıtlı bir bölge yükle",
|
||||||
|
"loading": "Yükleniyor",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Konum paylaşımı, donanım ve güncelleme aralığı.",
|
||||||
|
"locationTelemetry": "Konum telemetrisi",
|
||||||
|
"lockDestination": "Hedefi kilitle",
|
||||||
|
"longitude": "Boylam",
|
||||||
|
"mapStyle": "Harita Stili",
|
||||||
|
"max": "Maks",
|
||||||
|
"maxZoom": "Maks Yakınlaştırma",
|
||||||
|
"messaging": "Mesajlaşma",
|
||||||
|
"metHistory": "MET geçmişi",
|
||||||
|
"min": "Min",
|
||||||
|
"minZoom": "Min Yakınlaştırma",
|
||||||
|
"moveDown": "Aşağı taşı",
|
||||||
|
"moveUp": "Yukarı taşı",
|
||||||
|
"multiAckMode": "Çoklu ACK modu",
|
||||||
|
"multiDeviceWorkspaceManagement": "Çoklu cihaz çalışma alanı yönetimi",
|
||||||
|
"myLocation": "Konumum",
|
||||||
|
"nameAndTelemetryShared": "Ad ve telemetri diğer cihazlarla paylaşılır.",
|
||||||
|
"newVersionAvailable": "Yeni sürüm: {version}",
|
||||||
|
"newer": "Daha yeni",
|
||||||
|
"noNeighboursFound": "Komşu bulunamadı",
|
||||||
|
"noServersFound": "Sunucu bulunamadı",
|
||||||
|
"notSet": "Ayarlanmadı",
|
||||||
|
"notifications": "Bildirimler",
|
||||||
|
"offZeroSeconds": "Kapalı (0 sn)",
|
||||||
|
"offlineMaps": "Çevrimdışı Haritalar",
|
||||||
|
"older": "Daha eski",
|
||||||
|
"oneByteMode0": "1 bayt (mod 0)",
|
||||||
|
"oneHour": "1 saat",
|
||||||
|
"oneMinute": "1 dakika",
|
||||||
|
"openPacketLogs": "Paket günlüklerini aç",
|
||||||
|
"packetTypeHelp": "Paket türü yardımı",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Paylaşım bağlantısı veya Hex ilanı yapıştırın",
|
||||||
|
"pathHash": "Yol hash",
|
||||||
|
"pathSize": "Yol boyutu",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Çokgen",
|
||||||
|
"rawResponseCopied": "Ham yanıt kopyalandı",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Başarısızlık durumunda en yakın aktarıcı üzerinden yeniden gönder",
|
||||||
|
"resolveContact": "Kişiyi çözümle",
|
||||||
|
"routingRetriesAndDestinationLock": "Yönlendirme, yeniden denemeler ve hedef kilidi",
|
||||||
|
"samples": "Örnekler",
|
||||||
|
"satellites": "Uydular",
|
||||||
|
"saving": "Kaydediliyor...",
|
||||||
|
"search": "Ara",
|
||||||
|
"searchByNameOrToken": "Ada veya jetona göre ara",
|
||||||
|
"searchDiscoveredNodes": "Keşfedilen düğümleri ara",
|
||||||
|
"searchInCurrentFilter": "Geçerli filtrede ara",
|
||||||
|
"searchLogs": "Günlüklerde ara...",
|
||||||
|
"searchSensors": "Sensörleri ara",
|
||||||
|
"searchThisSection": "Bu bölümde ara",
|
||||||
|
"selectedRelays": "Seçili aktarıcılar",
|
||||||
|
"sensor": "Sensör",
|
||||||
|
"serialError": "Seri hata: {error}",
|
||||||
|
"setPathForContact": "{name} için yol ayarla",
|
||||||
|
"shareLiveGps": "Canlı GPS paylaş",
|
||||||
|
"shareMyTiles": "Döşemelerimi paylaş",
|
||||||
|
"sharingLocationFromPhone": "Bu kanalda telefondan konum paylaşılıyor.",
|
||||||
|
"sharingLocationFromRadio": "Bu kanalda radyodan konum paylaşılıyor.",
|
||||||
|
"signalDbm": "Sinyal {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "Boyut, sıkıştırma ve önizleme",
|
||||||
|
"skip": "Atla",
|
||||||
|
"smallerFileSize": "Daha küçük dosya boyutu",
|
||||||
|
"sort": "Sırala",
|
||||||
|
"stoppedSharingLocation": "Bu kanalda konum paylaşımı durduruldu.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Ön plandayken bildirimleri bastır",
|
||||||
|
"tabsAndNavigation": "Sekmeler ve Gezinme",
|
||||||
|
"templatesAndTutorials": "Şablonlar ve öğreticiler",
|
||||||
|
"tenMinutes": "10 dakika",
|
||||||
|
"tenSeconds": "10 saniye",
|
||||||
|
"testSendUpdate": "Test güncelleme gönder",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Tema, dil ve görüntüleme tercihleri",
|
||||||
|
"thirtyMinutes": "30 dakika",
|
||||||
|
"thirtySeconds": "30 saniye",
|
||||||
|
"threeBytesMode2": "3 bayt (mod 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Trafik istatistikleri, mesaj geçmişi ve geliştirici araçları",
|
||||||
|
"twoBytesMode1": "2 bayt (mod 1)",
|
||||||
|
"twoMinutes": "2 dakika",
|
||||||
|
"undo": "Geri al",
|
||||||
|
"updated": "Güncellendi",
|
||||||
|
"useContactFlags": "Kişi bayraklarını kullan",
|
||||||
|
"useSavedCoordinates": "Kayıtlı koordinatları kullan",
|
||||||
|
"view": "Görüntüle",
|
||||||
|
"viewNeighbours": "Komşuları Görüntüle",
|
||||||
|
"viewPublicStats": "Herkese açık istatistikleri görüntüle",
|
||||||
|
"setRegionScope": "Bölge kapsamını ayarla",
|
||||||
|
"regionScope": "Bölge kapsamı",
|
||||||
|
"regionScopeNone": "Yok (genel)",
|
||||||
|
"clearRegionScope": "Kapsamı temizle",
|
||||||
|
"regionScopeWarning": "Yalnızca bu bölgeye izin veren aktarıcılar iletecektir.",
|
||||||
|
"discoverRegions": "Aktarıcılardan keşfet",
|
||||||
|
"discoveringRegions": "Bölgeler keşfediliyor...",
|
||||||
|
"enterRegionName": "Bölge adı (ör. auckland)",
|
||||||
|
"noRegionsFound": "Bu aktarıcıda bölge bulunamadı.",
|
||||||
|
"regionScopeSet": "Bölge kapsamı {name} olarak ayarlandı",
|
||||||
|
"regionScopeCleared": "Bölge kapsamı temizlendi",
|
||||||
|
"simpleMode": "Basit mod",
|
||||||
|
"simpleModeDescription": "Yalnızca favori kişilerle sohbet ve harita",
|
||||||
|
"simpleModeRequiresLockedDestination": "Basit modu kullanmak için önce bir kanalla \"Hedefi kilitle\" seçeneğini etkinleştirin",
|
||||||
|
"noFavouriteContacts": "Favori kişi yok",
|
||||||
|
"selectDestinationFirst": "Önce bir kanal, kişi veya oda seçin",
|
||||||
|
"recipientNoLongerAvailable": "Alıcı artık mevcut değil",
|
||||||
|
"failedToSendMessage": "Gönderilemedi: {error}",
|
||||||
|
"tictactoeDirectOnly": "XOX yalnızca doğrudan mesajlarda çalışır. Önce bir kişi seçin.",
|
||||||
|
"deviceKeyUnavailable": "Cihaz anahtarı mevcut değil",
|
||||||
|
"imageCompressionFailed": "Resim sıkıştırma başarısız",
|
||||||
|
"imageFragmentationFailed": "Resim parçalama başarısız",
|
||||||
|
"failedToAnnounceImage": "Resim duyurulamadı",
|
||||||
|
"noRecipientSelected": "Alıcı seçilmedi",
|
||||||
|
"imageSendFailed": "Resim gönderilemedi",
|
||||||
|
"microphonePermissionRequiredForVoice": "Sesli mesajlar için mikrofon izni gerekli",
|
||||||
|
"recordingDiscarded": "Kayıt silindi",
|
||||||
|
"publicChannelMediaWarning": "Genel kanala {mediaType} göndermek üzeresiniz. Mesh ağındaki herkes bunu alabileceği için bu önerilmez. Bunu özellikle istemiyorsanız özel veya etiketli bir kanal seçin.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Konumunuzu paylaşmak için özel bir kanal seçin.",
|
||||||
|
"stopSharingMyLocation": "Konumumu paylaşmayı durdur",
|
||||||
|
"shareMyLocation": "Konumumu paylaş",
|
||||||
|
"stopRecording": "Kaydı durdur",
|
||||||
|
"recordVoice": "Sesli mesaj kaydet",
|
||||||
|
"moreActions": "Diğer işlemler",
|
||||||
|
"noMessagesInFilter": "Bu filtrede mesaj yok",
|
||||||
|
"noMatchesInFilter": "Bu filtrede eşleşme yok",
|
||||||
|
"syncFailed": "Senkronizasyon başarısız: {error}",
|
||||||
|
"invalidLink": "Geçersiz bağlantı",
|
||||||
|
"cannotOpenLink": "Bağlantı açılamıyor",
|
||||||
|
"failedToOpenLink": "Bağlantı açılamadı",
|
||||||
|
"failedToResendMessage": "Mesaj yeniden gönderilemedi",
|
||||||
|
"retryFailed": "Yeniden deneme başarısız: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Çizimi ve mesajı sil",
|
||||||
|
"lastEchoRelay": "Son yankı: röle",
|
||||||
|
"lastEchoPath": "Son yankı: yol",
|
||||||
|
"lastEchoBytesReport": "Son yankı: bayt raporu",
|
||||||
|
"noGpsCoordinatesAvailable": "Kullanılabilir GPS koordinatı yok",
|
||||||
|
"notASarMarker": "Bu bir SAR işareti değil",
|
||||||
|
"failedToSaveTemplate": "Şablon kaydedilemedi: {error}",
|
||||||
|
"drawingNotFound": "Çizim bulunamadı",
|
||||||
|
"unknownDrawingType": "Bilinmeyen çizim türü",
|
||||||
|
"customMapMarker": "Özel harita işareti",
|
||||||
|
"customMapPointLabel": "Nokta: {point}",
|
||||||
|
"mapIdLabel": "Harita ID: {id}",
|
||||||
|
"cannotFetchVoice": "Ses alınamıyor",
|
||||||
|
"senderContactUnknown": "Gönderenin kişisi bilinmiyor. Önce kişileri senkronize edin.",
|
||||||
|
"senderRouteUnknown": "Gönderenin rotası bilinmiyor. Önce kişileri/yolu senkronize edin.",
|
||||||
|
"messageTooFar": "Mesaj çok uzakta ({hops} atlama, en fazla {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Gönderenin rotası yol denetimine yanıt vermedi. Kişileri/yolu senkronize edip yeniden deneyin.",
|
||||||
|
"senderRouteNoRawResponse": "Gönderenin rotası ham aktarım yolunda yanıt vermedi.",
|
||||||
|
"voiceFetchOverHops": "{hops} atlama üzerinden ses alma biraz sürebilir.",
|
||||||
|
"voiceReceiveCanceled": "Ses alımı iptal edildi",
|
||||||
|
"fetchingMissingVoiceFragments": "Eksik ses parçaları alınıyor",
|
||||||
|
"receivingVoice": "Ses alınıyor",
|
||||||
|
"cannotFetchImage": "Resim alınamıyor",
|
||||||
|
"imageFetchOverHops": "{hops} atlama üzerinden resim alma biraz sürebilir.",
|
||||||
|
"imageAlreadyBeingReceived": "Resim zaten alınıyor",
|
||||||
|
"loadImage": "Resmi yükle",
|
||||||
|
"imageFetchFailedToSendRequest": "Resim isteği gönderilemedi",
|
||||||
|
"imageUnavailable": "Resim şu anda kullanılamıyor",
|
||||||
|
"imageFetchTimedOut": "Resim alma zaman aşımına uğradı",
|
||||||
|
"imageReceiveCanceled": "Resim alımı iptal edildi",
|
||||||
|
"closeImagePreview": "Resim önizlemesini kapat",
|
||||||
|
"recordingReleaseToSend": "Kaydediliyor... göndermek için bırakın",
|
||||||
|
"sendingVoice": "Ses gönderiliyor...",
|
||||||
|
"sendLongPressToRecordVoice": "Gönder (ses kaydı için basılı tutun)",
|
||||||
|
"discardRecording": "Kaydı sil",
|
||||||
|
"hopCount": "{count, plural, other{{count} atlama}}",
|
||||||
|
"channelsSectionDescription": "Yakındaki mesh trafiği için yayın kanalları",
|
||||||
|
"roomsSectionDescription": "Sürekli ekip koordinasyonu için ortak alanlar",
|
||||||
|
"contactsSectionDescription": "Doğrudan ulaşabileceğiniz kişiler ve cihazlar",
|
||||||
|
"noRecentChatters": "Yakın zamanda konuşan yok",
|
||||||
|
"public": "Genel",
|
||||||
|
"failedToSendTicTacToeMove": "XOX hamlesi gönderilemedi",
|
||||||
|
"youWon": "Kazandınız",
|
||||||
|
"opponentWon": "Rakip kazandı",
|
||||||
|
"gameDraw": "Berabere",
|
||||||
|
"yourTurn": "Sıra sizde",
|
||||||
|
"opponentTurn": "Sıra rakipte",
|
||||||
|
"pleaseEnterValidCoordinates": "Geçerli koordinatlar girin",
|
||||||
|
"locationNotAvailable": "Konum mevcut değil",
|
||||||
|
"customCaveMapPoint": "Özel mağara haritası noktası",
|
||||||
|
"mapPoint": "Harita noktası",
|
||||||
|
"fetchingMissingImageFragments": "Eksik parçalar alınıyor…",
|
||||||
|
"loadingImage": "Yükleniyor…",
|
||||||
|
"receivingImage": "Alınıyor…",
|
||||||
|
"tapToLoad": "Yüklemek için dokunun",
|
||||||
|
"connectDevice": "Cihaza bağlan",
|
||||||
|
"chooseTransportSubtitle": "Bluetooth, WiFi veya Seri aktarım seçin",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Ağ",
|
||||||
|
"serial": "Seri",
|
||||||
|
"scan": "Tara",
|
||||||
|
"pressScanToSearchForDevices": "Yakındaki cihazları aramak için tara düğmesine basın",
|
||||||
|
"failedToConnectToDevice": "{name} cihazına bağlanılamadı",
|
||||||
|
"unknownDevice": "Bilinmeyen cihaz",
|
||||||
|
"showingCachedResultsTapRefresh": "Önbelleğe alınmış sonuçlar gösteriliyor. Yeniden taramak için yenile düğmesine dokunun.",
|
||||||
|
"scanningLocalNetworkOnPort": "Yerel ağda {port} bağlantı noktasındaki MeshCore WiFi cihazları taranıyor",
|
||||||
|
"cancelScanAndAddServer": "Taramayı iptal et ve sunucu ekle",
|
||||||
|
"addServer": "Sunucu ekle",
|
||||||
|
"scanningProgressIps": "Taranıyor... {scanned}/{total} IP",
|
||||||
|
"cancelAndEnterManually": "İptal et ve elle gir",
|
||||||
|
"noRecentOrDiscoveredServers": "Henüz son kullanılan veya keşfedilen sunucu yok",
|
||||||
|
"recentlyUsed": "Son kullanılanlar",
|
||||||
|
"connecting": "Bağlanıyor...",
|
||||||
|
"discoveredOnThisNetwork": "Bu ağda keşfedildi",
|
||||||
|
"serverNoLongerAvailable": "{host}:{port} adresindeki sunucu artık kullanılamıyor. Etkin sunucuları bulmak için lütfen yeniden tarayın.",
|
||||||
|
"failedToConnectToHost": "{host}:{port} adresine bağlanılamadı",
|
||||||
|
"enterValidIpAddress": "Geçerli bir IP adresi girin",
|
||||||
|
"enterValidTcpPort": "Geçerli bir TCP bağlantı noktası girin",
|
||||||
|
"ipAddress": "IP adresi",
|
||||||
|
"tcpPort": "TCP bağlantı noktası",
|
||||||
|
"customServerPort": "Özel sunucu bağlantı noktası",
|
||||||
|
"chat": "Sohbet",
|
||||||
|
"restoringPreviousLink": "Önceki bağlantı geri yükleniyor",
|
||||||
|
"noDeviceConnected": "Bağlı cihaz yok",
|
||||||
|
"sendAdvert": "Kişi bilgimi gönder",
|
||||||
|
"advertMode": "Kişi bilgimi gönder",
|
||||||
|
"advertModeSubtitle": "Kişi bilgilerinizin ne kadar uzağa gideceğini seçin.",
|
||||||
|
"floodAdvertSent": "Kişi bilgisi tüm mesh ağına gönderildi",
|
||||||
|
"directAdvertSent": "Kişi bilgisi yakındaki cihazlara gönderildi"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "Button to restart BLE scanning"
|
"description": "Button to restart BLE scanning"
|
||||||
},
|
},
|
||||||
"tapToConnect": "Торкніться, щоб підключитися",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "Subtitle text for device in scan list"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "Пристрій не підключено",
|
"deviceNotConnected": "Пристрій не підключено",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "Error message when device is not connected"
|
"description": "Error message when device is not connected"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "Setting to show RX/TX indicators"
|
"description": "Setting to show RX/TX indicators"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "Показувати індикатори активності пакетів у верхній панелі",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "Description for RX/TX indicators setting"
|
|
||||||
},
|
|
||||||
"disableMap": "Вимкнути мапу",
|
"disableMap": "Вимкнути мапу",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "Setting to disable the map tab"
|
"description": "Setting to disable the map tab"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "Приховати вкладку мапи для зменшення споживання батареї",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "Description for disable map setting"
|
|
||||||
},
|
|
||||||
"language": "Мова",
|
"language": "Мова",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "Language setting label"
|
"description": "Language setting label"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "App version label"
|
"description": "App version label"
|
||||||
},
|
},
|
||||||
"appName": "Назва застосунку",
|
|
||||||
"@appName": {
|
|
||||||
"description": "App name label"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "Про MeshCore SAR",
|
"aboutMeshCoreSar": "Про MeshCore SAR",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "About dialog title"
|
"description": "About dialog title"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "Action to reset contact path for re-routing"
|
"description": "Action to reset contact path for re-routing"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "Публічний ключ скопійовано в буфер обміну",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "Success message when public key is copied"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} скопійовано в буфер обміну",
|
"copiedToClipboard": "{label} скопійовано в буфер обміну",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "Success message when a value is copied to clipboard",
|
"description": "Success message when a value is copied to clipboard",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "Last seen label"
|
"description": "Last seen label"
|
||||||
},
|
},
|
||||||
"roomStatus": "Стан кімнати",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "Room status section header"
|
|
||||||
},
|
|
||||||
"loginStatus": "Стан входу",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "Login status label"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "Не увійшли",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "Not logged in status"
|
|
||||||
},
|
|
||||||
"adminAccess": "Доступ адміністратора",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "Admin access label"
|
|
||||||
},
|
|
||||||
"yes": "Так",
|
"yes": "Так",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "Yes answer"
|
"description": "Yes answer"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "Permissions label"
|
"description": "Permissions label"
|
||||||
},
|
},
|
||||||
"passwordSaved": "Пароль збережено",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "Password saved label"
|
|
||||||
},
|
|
||||||
"locationColon": "Місцезнаходження:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "Location section header"
|
|
||||||
},
|
|
||||||
"telemetry": "Телеметрія",
|
"telemetry": "Телеметрія",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "Telemetry section header"
|
"description": "Telemetry section header"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "GPS from telemetry label"
|
"description": "GPS from telemetry label"
|
||||||
},
|
},
|
||||||
"updated": "Оновлено",
|
|
||||||
"@updated": {
|
|
||||||
"description": "Updated timestamp label"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "Маршрут для {name} скинуто. Наступне повідомлення знайде новий маршрут.",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "Info message after path reset",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "Повторно увійти до кімнати",
|
"reLoginToRoom": "Повторно увійти до кімнати",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "Button to re-login to room"
|
"description": "Button to re-login to room"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "Label for radio frequency field"
|
"description": "Label for radio frequency field"
|
||||||
},
|
},
|
||||||
"frequencyExample": "напр., 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "Helper text example for frequency"
|
|
||||||
},
|
|
||||||
"bandwidth": "Ширина смуги",
|
"bandwidth": "Ширина смуги",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "Label for bandwidth dropdown"
|
"description": "Label for bandwidth dropdown"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "Trail controls tooltip"
|
"description": "Trail controls tooltip"
|
||||||
},
|
},
|
||||||
"contactTrails": "Сліди контактів",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "Contact trails section header"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "Показати всі сліди контактів",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "Toggle label to show all contact trails"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "Немає контактів з історією місцезнаходження",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "Subtitle when no contacts have trails"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "Показуються сліди для {count} контактів",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "Subtitle showing number of contacts with trails",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "Окремі сліди контактів",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "Expansion tile title for individual contact trails"
|
|
||||||
},
|
|
||||||
"deviceInformation": "Інформація про пристрій",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "Device information section header"
|
|
||||||
},
|
|
||||||
"bleName": "Назва BLE",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "Bluetooth Low Energy device name label"
|
|
||||||
},
|
|
||||||
"meshName": "Назва mesh",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh network name label"
|
|
||||||
},
|
|
||||||
"notSet": "Не задано",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "Label when a value is not set"
|
|
||||||
},
|
|
||||||
"model": "Модель",
|
"model": "Модель",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "Device model label"
|
"description": "Device model label"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "Version label"
|
"description": "Version label"
|
||||||
},
|
},
|
||||||
"buildDate": "Дата збірки",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "Firmware build date label"
|
|
||||||
},
|
|
||||||
"firmware": "Прошивка",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "Firmware label"
|
|
||||||
},
|
|
||||||
"maxContacts": "Макс. контактів",
|
"maxContacts": "Макс. контактів",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "Maximum contacts capacity label"
|
"description": "Maximum contacts capacity label"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "Public information section header"
|
"description": "Public information section header"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Назва mesh-мережі",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh network name field label"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "Ім’я, що транслюється в mesh-оголошеннях",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Helper text for mesh network name field"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "Телеметрія та поширення місцезнаходження",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "Telemetry and location sharing toggle label"
|
|
||||||
},
|
|
||||||
"lat": "Шир.",
|
|
||||||
"@lat": {
|
|
||||||
"description": "Latitude field label (short form)"
|
|
||||||
},
|
|
||||||
"lon": "Довг.",
|
|
||||||
"@lon": {
|
|
||||||
"description": "Longitude field label (short form)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "Використати поточне місцезнаходження",
|
"useCurrentLocation": "Використати поточне місцезнаходження",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "Tooltip for use current location button"
|
"description": "Tooltip for use current location button"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} скопійовано в буфер обміну",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "Short success message when copying to clipboard",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "Не вдалося зберегти: {error}",
|
"failedToSave": "Не вдалося зберегти: {error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "Generic error message for save failures",
|
"description": "Generic error message for save failures",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "SAR templates menu title"
|
"description": "SAR templates menu title"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "Керувати шаблонами SAR",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "Subtitle for SAR templates settings"
|
|
||||||
},
|
|
||||||
"addTemplate": "Додати шаблон",
|
"addTemplate": "Додати шаблон",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "Button to add new SAR template"
|
"description": "Button to add new SAR template"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "OK button label"
|
"description": "OK button label"
|
||||||
},
|
},
|
||||||
"permissionsSection": "Дозволи",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "Permissions section header"
|
|
||||||
},
|
|
||||||
"locationPermission": "Дозвіл на геолокацію",
|
"locationPermission": "Дозвіл на геолокацію",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "Location permission label"
|
"description": "Location permission label"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "Welcome wizard first page title"
|
"description": "Welcome wizard first page title"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "Потужний інструмент офлайн-комунікації для операцій пошуку та рятування. Зв’язуйтеся зі своєю командою за допомогою mesh-радіо, коли традиційні мережі недоступні.",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "Welcome wizard first page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "Підключення до радіо",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "Wizard connecting page title"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "Підключіть смартфон до радіопристрою MeshCore через Bluetooth, щоб почати спілкування поза мережею.",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "Wizard connecting page description"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "Скануйте сусідні пристрої MeshCore",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "Wizard connecting feature 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "Сполучіть свій радіопристрій через Bluetooth",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "Wizard connecting feature 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "Працює повністю офлайн - інтернет не потрібен",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "Wizard connecting feature 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "Канали",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "Wizard channel page title"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "Транслюйте повідомлення всім у каналі - ідеально для загальнокомандних оголошень і координації.",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "Wizard channel page description"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "Публічний канал для загального спілкування команди",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "Wizard channel feature 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "Створюйте власні канали для окремих груп",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "Wizard channel feature 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "Повідомлення автоматично ретранслюються mesh-мережею",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "Wizard channel feature 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "Контакти",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "Wizard contacts page title"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "Члени вашої команди з’являються автоматично, коли приєднуються до mesh-мережі. Надсилайте їм прямі повідомлення або переглядайте їхнє місцезнаходження.",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "Wizard contacts page description"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "Контакти виявляються автоматично",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "Wizard contacts feature 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "Надсилайте приватні прямі повідомлення",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "Wizard contacts feature 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "Переглядайте рівень батареї та час останньої появи",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "Wizard contacts feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "Мапа та місцезнаходження",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "Wizard map page title"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "Відстежуйте команду в реальному часі та позначайте важливі місця для операцій пошуку та рятування.",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "Wizard map page description"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "Маркери SAR для знайдених людей, пожеж і зон збору",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "Wizard map feature 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "GPS-відстеження членів команди в реальному часі",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "Wizard map feature 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "Завантажуйте офлайн-мапи для віддалених районів",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "Wizard map feature 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "Малюйте фігури та діліться тактичною інформацією",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "Wizard map feature 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "Переглянути вступний посібник",
|
"viewWelcomeTutorial": "Переглянути вступний посібник",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "Settings option to re-show welcome wizard"
|
"description": "Settings option to re-show welcome wizard"
|
||||||
@@ -2328,5 +2093,525 @@
|
|||||||
"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": "Смуговий фільтр голосу",
|
||||||
|
"active": "Активно",
|
||||||
|
"addContact": "Додати контакт",
|
||||||
|
"all": "Усі",
|
||||||
|
"clearAllLabel": "Очистити все",
|
||||||
|
"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": "Поділитися",
|
||||||
|
"trace": "Трасування",
|
||||||
|
"discovery": "Виявлення",
|
||||||
|
"discoverRepeaters": "Виявити повторювачі",
|
||||||
|
"discoverSensors": "Виявити датчики",
|
||||||
|
"repeaterDiscoverySent": "Виявлення повторювачів надіслано",
|
||||||
|
"sensorDiscoverySent": "Виявлення датчиків надіслано",
|
||||||
|
"clearedPendingDiscoveries": "Очікувані виявлення очищено.",
|
||||||
|
"autoDiscovery": "Автоматичне виявлення",
|
||||||
|
"enableAutomaticAdding": "Увімкнути автоматичне додавання",
|
||||||
|
"autoaddRepeaters": "Автододавання повторювачів",
|
||||||
|
"autoaddRoomServers": "Автододавання серверів кімнат",
|
||||||
|
"autoaddSensors": "Автододавання датчиків",
|
||||||
|
"autoaddUsers": "Автододавання користувачів",
|
||||||
|
"overwriteOldestWhenFull": "Перезаписати найстаріші при заповненні",
|
||||||
|
"storage": "Сховище",
|
||||||
|
"dangerZone": "Небезпечна зона",
|
||||||
|
"profiles": "Профілі",
|
||||||
|
"favourites": "Обране",
|
||||||
|
"sensors": "Датчики",
|
||||||
|
"others": "Інші",
|
||||||
|
"gpsModule": "Модуль GPS",
|
||||||
|
"liveTraffic": "Живий трафік",
|
||||||
|
"repeatersMap": "Карта повторювачів",
|
||||||
|
"blePacketLogs": "Журнали BLE-пакетів",
|
||||||
|
"onlineTraceDatabase": "База трасувань",
|
||||||
|
"routePathByteSize": "Розмір шляху в байтах",
|
||||||
|
"messageNotifications": "Сповіщення про повідомлення",
|
||||||
|
"sarAlerts": "SAR-сповіщення",
|
||||||
|
"discoveryNotifications": "Сповіщення про виявлення",
|
||||||
|
"updateNotifications": "Сповіщення про оновлення",
|
||||||
|
"muteWhileAppIsOpen": "Без звуку при відкритому додатку",
|
||||||
|
"disableContacts": "Вимкнути контакти",
|
||||||
|
"enableSensorsTab": "Увімкнути вкладку Датчики",
|
||||||
|
"enableProfiles": "Увімкнути профілі",
|
||||||
|
"nearestRepeaterFallback": "Найближчий повторювач як резерв",
|
||||||
|
"deleteAllStoredMessageHistory": "Видалити всю історію повідомлень",
|
||||||
|
"messageFontSize": "Розмір шрифту повідомлень",
|
||||||
|
"rotateMapWithHeading": "Обертати карту за напрямком",
|
||||||
|
"showMapDebugInfo": "Показати налагоджувальну інфо карти",
|
||||||
|
"openMapInFullscreen": "Відкрити карту на повний екран",
|
||||||
|
"showSarMarkersLabel": "Показати SAR-маркери",
|
||||||
|
"hideRepeatersOnMap": "Сховати повторювачі на карті",
|
||||||
|
"setMapScale": "Встановити масштаб карти",
|
||||||
|
"customMapScaleSaved": "Користувацький масштаб карти збережено",
|
||||||
|
"voiceBitrate": "Бітрейт голосу",
|
||||||
|
"voiceCompressor": "Компресор голосу",
|
||||||
|
"voiceLimiter": "Лімітер голосу",
|
||||||
|
"micAutoGain": "Автопідсилення мікрофона",
|
||||||
|
"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": "Тихо",
|
||||||
|
"busy": "Зайнято",
|
||||||
|
"searchMessages": "Пошук повідомлень",
|
||||||
|
"sendImageFromGallery": "Надіслати зображення з галереї",
|
||||||
|
"takePhoto": "Зробити фото",
|
||||||
|
"allMessages": "Усі повідомлення",
|
||||||
|
"sendToPublicChannel": "Надіслати в публічний канал?",
|
||||||
|
"selectMarkerTypeAndDestination": "Виберіть тип маркера та призначення",
|
||||||
|
"noDestinationsAvailableLabel": "Немає доступних призначень",
|
||||||
|
"image": "Зображення",
|
||||||
|
"format": "Формат",
|
||||||
|
"dimensions": "Розміри",
|
||||||
|
"segments": "Сегменти",
|
||||||
|
"transfers": "Передачі",
|
||||||
|
"downloadedBy": "Завантажено",
|
||||||
|
"saveDiscoverySettings": "Зберегти налаштування виявлення",
|
||||||
|
"savePublicInfo": "Зберегти публічну інформацію",
|
||||||
|
"saveRadioSettings": "Зберегти налаштування радіо",
|
||||||
|
"savePath": "Зберегти шлях",
|
||||||
|
"wipeDeviceData": "Стерти дані пристрою",
|
||||||
|
"wipeDevice": "Стерти пристрій",
|
||||||
|
"destructiveDeviceActions": "Деструктивні дії пристрою.",
|
||||||
|
"chooseAPresetOrFinetuneCustomRadioSettings": "Оберіть пресет або налаштуйте радіо вручну.",
|
||||||
|
"used": "Використано",
|
||||||
|
"total": "Загалом",
|
||||||
|
"renameValue": "Перейменувати значення",
|
||||||
|
"customizeFields": "Налаштувати поля",
|
||||||
|
"livePreview": "Попередній перегляд наживо",
|
||||||
|
"refreshSchedule": "Розклад оновлення",
|
||||||
|
"noResponse": "Немає відповіді",
|
||||||
|
"refreshing": "Оновлення",
|
||||||
|
"unavailable": "Недоступно",
|
||||||
|
"pickARelayOrNodeToWatchInSensors": "Оберіть реле або вузол для спостереження.",
|
||||||
|
"publicKeyLabel": "Публічний ключ",
|
||||||
|
"alreadyInContacts": "Вже в контактах",
|
||||||
|
"connectToADeviceBeforeAddingContacts": "Підключіться до пристрою перед додаванням контактів",
|
||||||
|
"fromContacts": "З контактів",
|
||||||
|
"onlineOnly": "Лише онлайн",
|
||||||
|
"inBoth": "В обох",
|
||||||
|
"source": "Джерело",
|
||||||
|
"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": "Немає контактів пристрою для очищення.",
|
||||||
|
"autoSend": "Автовідправлення",
|
||||||
|
"autoSendDescription": "Надіслати поточним маршрутом.",
|
||||||
|
"sendDirect": "Пряме надсилання",
|
||||||
|
"sendDirectDescription": "Надіслати безпосередньо цьому контакту.",
|
||||||
|
"sendFlood": "Надсилання лавиною",
|
||||||
|
"sendFloodDescription": "Надіслати через усі ретранслятори.",
|
||||||
|
"postConnectDiscoveryTitle": "Виявити ретранслятори зараз?",
|
||||||
|
"postConnectDiscoveryDescription": "Запустіть виявлення ретрансляторів одразу після підключення, щоб побачити найближчі вузли MeshCore та швидше додати їх до своєї мережі.",
|
||||||
|
"aToZ": "А-Я",
|
||||||
|
"add": "Додати",
|
||||||
|
"addAdditionalDetails": "Додати додаткові деталі",
|
||||||
|
"addPeer": "Додати вузол",
|
||||||
|
"addPeerManually": "Додати вузол вручну",
|
||||||
|
"addedToContacts": "{name} додано до контактів",
|
||||||
|
"addedToSensors": "{name} додано до сенсорів",
|
||||||
|
"allowAll": "Дозволити все",
|
||||||
|
"anonymousRxStats": "Анонімна статистика RX",
|
||||||
|
"appearance": "Зовнішній вигляд",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "Базова телеметрія",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "Сповіщення зумером",
|
||||||
|
"cancelImageReceive": "Скасувати отримання зображення",
|
||||||
|
"choose": "Обрати",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "Оберіть, які вкладки та розділи контактів показувати",
|
||||||
|
"clearCache": "Очистити кеш",
|
||||||
|
"clearHistory": "Очистити історію",
|
||||||
|
"clearLiveView": "Очистити живий перегляд",
|
||||||
|
"clearLogs": "Очистити журнали",
|
||||||
|
"clearOfflineCache": "Очистити офлайн-кеш?",
|
||||||
|
"clearOverlay": "Очистити накладання",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "Очистити маршрут лише після невдачі всіх спроб",
|
||||||
|
"codecBitrateAndAudioProcessing": "Кодек, бітрейт та обробка звуку",
|
||||||
|
"connectByIpAddress": "Підключити за IP-адресою",
|
||||||
|
"controlWhichAlertsYouReceive": "Керуйте сповіщеннями, які ви отримуєте",
|
||||||
|
"copyFullHex": "Копіювати повний Hex",
|
||||||
|
"copyRawResponse": "Копіювати необроблену відповідь",
|
||||||
|
"couldNotLoadMetHistory": "Не вдалося завантажити історію MET",
|
||||||
|
"customizeSensor": "Налаштувати {name}",
|
||||||
|
"data": "Дані",
|
||||||
|
"deleteGroup": "Видалити групу",
|
||||||
|
"deleteStyleConfirm": "Видалити {name}?",
|
||||||
|
"deleteThisDrawing": "Видалити цей {type}?",
|
||||||
|
"deny": "Відхилити",
|
||||||
|
"deviceInfo": "Інформація про пристрій",
|
||||||
|
"deviceSettings": "Налаштування пристрою",
|
||||||
|
"deviceTimeSynced": "Час пристрою синхронізовано з цим телефоном.",
|
||||||
|
"disableFastGpsPublishing": "Вимкнути швидку публікацію GPS",
|
||||||
|
"displayMarkersAndTraceDatabase": "Відображення, маркери та база даних трасувань",
|
||||||
|
"download": "Завантажити",
|
||||||
|
"environmentalTelemetry": "Телеметрія довкілля",
|
||||||
|
"errorCheckingForUpdates": "Помилка перевірки оновлень: {error}",
|
||||||
|
"errorGeneric": "Помилка: {error}",
|
||||||
|
"errorOpeningDownload": "Помилка відкриття завантаження: {error}",
|
||||||
|
"exportLogs": "Експортувати журнали",
|
||||||
|
"extraAggressiveAvifCompression": "Надагресивне стиснення AVIF",
|
||||||
|
"failedToAddContact": "Не вдалося додати контакт: {error}",
|
||||||
|
"failedToAddName": "Не вдалося додати {name}: {error}",
|
||||||
|
"failedToClearChannels": "Не вдалося очистити канали: {error}",
|
||||||
|
"failedToClearContacts": "Не вдалося очистити контакти: {error}",
|
||||||
|
"failedToConnectViaSerial": "Не вдалося підключитися через послідовний порт",
|
||||||
|
"failedToLoadPreviewImage": "Не вдалося завантажити попередній перегляд: {error}",
|
||||||
|
"failedToLoadTrace": "Не вдалося завантажити трасування: {error}",
|
||||||
|
"failedToSendSarMarker": "Не вдалося надіслати SAR маркер: {error}",
|
||||||
|
"failedToSetBuzzerMode": "Не вдалося встановити режим зумера: {error}",
|
||||||
|
"failedToSetGpsMode": "Не вдалося встановити режим GPS: {error}",
|
||||||
|
"failedToSyncDeviceTime": "Не вдалося синхронізувати час пристрою: {error}",
|
||||||
|
"failedToWipeDeviceData": "Не вдалося стерти дані пристрою: {error}",
|
||||||
|
"fastGpsTargetChannel": "Цільовий канал швидкого GPS",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "Швидкі оновлення, пороги та дозволи",
|
||||||
|
"fetchingCatalog": "Завантаження каталогу...",
|
||||||
|
"fifteenMinutes": "15 хвилин",
|
||||||
|
"fifteenSeconds": "15 секунд",
|
||||||
|
"filterByDirection": "Фільтрувати за напрямком",
|
||||||
|
"finish": "Завершити",
|
||||||
|
"fiveMinutes": "5 хвилин",
|
||||||
|
"fiveSeconds": "5 секунд",
|
||||||
|
"fix": "Фіксація",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "Політика оголошень GPS",
|
||||||
|
"gpsAndLocation": "GPS та місцезнаходження",
|
||||||
|
"gpsDiagnostics": "Діагностика GPS",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "Допомога",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "Приховано",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "Як радіо автоматично додає виявлені вузли.",
|
||||||
|
"images": "Зображення",
|
||||||
|
"importProfile": "Імпортувати профіль",
|
||||||
|
"lastFix": "Остання фіксація",
|
||||||
|
"latest": "Найновіші",
|
||||||
|
"latitude": "Широта",
|
||||||
|
"loadASavedRegion": "Завантажити збережений регіон",
|
||||||
|
"loading": "Завантаження",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "Обмін місцезнаходженням, обладнання та інтервал оновлення.",
|
||||||
|
"locationTelemetry": "Телеметрія місцезнаходження",
|
||||||
|
"lockDestination": "Зафіксувати призначення",
|
||||||
|
"longitude": "Довгота",
|
||||||
|
"mapStyle": "Стиль мапи",
|
||||||
|
"max": "Макс",
|
||||||
|
"maxZoom": "Макс. масштаб",
|
||||||
|
"messaging": "Обмін повідомленнями",
|
||||||
|
"metHistory": "Історія MET",
|
||||||
|
"min": "Мін",
|
||||||
|
"minZoom": "Мін. масштаб",
|
||||||
|
"moveDown": "Перемістити вниз",
|
||||||
|
"moveUp": "Перемістити вгору",
|
||||||
|
"multiAckMode": "Режим мульти-ACK",
|
||||||
|
"multiDeviceWorkspaceManagement": "Керування робочим простором кількох пристроїв",
|
||||||
|
"myLocation": "Моє місцезнаходження",
|
||||||
|
"nameAndTelemetryShared": "Ім'я та телеметрія надсилаються іншим пристроям.",
|
||||||
|
"newVersionAvailable": "Нова версія: {version}",
|
||||||
|
"newer": "Новіші",
|
||||||
|
"noNeighboursFound": "Сусідів не знайдено",
|
||||||
|
"noServersFound": "Серверів не знайдено",
|
||||||
|
"notSet": "Не встановлено",
|
||||||
|
"notifications": "Сповіщення",
|
||||||
|
"offZeroSeconds": "Вимк (0 с)",
|
||||||
|
"offlineMaps": "Офлайн-мапи",
|
||||||
|
"older": "Старіші",
|
||||||
|
"oneByteMode0": "1 байт (режим 0)",
|
||||||
|
"oneHour": "1 година",
|
||||||
|
"oneMinute": "1 хвилина",
|
||||||
|
"openPacketLogs": "Відкрити журнали пакетів",
|
||||||
|
"packetTypeHelp": "Довідка типів пакетів",
|
||||||
|
"pasteShareLinkOrHexAdvert": "Вставте посилання або hex-оголошення",
|
||||||
|
"pathHash": "Хеш шляху",
|
||||||
|
"pathSize": "Розмір шляху",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "Полігон",
|
||||||
|
"rawResponseCopied": "Необроблену відповідь скопійовано",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "Повторно надіслати через найближчий ретранслятор у разі невдачі",
|
||||||
|
"resolveContact": "Розв'язати контакт",
|
||||||
|
"routingRetriesAndDestinationLock": "Маршрутизація, повторні спроби та фіксація призначення",
|
||||||
|
"samples": "Зразки",
|
||||||
|
"satellites": "Супутники",
|
||||||
|
"saving": "Збереження...",
|
||||||
|
"search": "Пошук",
|
||||||
|
"searchByNameOrToken": "Шукати за іменем або токеном",
|
||||||
|
"searchDiscoveredNodes": "Шукати виявлені вузли",
|
||||||
|
"searchInCurrentFilter": "Шукати в поточному фільтрі",
|
||||||
|
"searchLogs": "Шукати в журналах...",
|
||||||
|
"searchSensors": "Шукати сенсори",
|
||||||
|
"searchThisSection": "Шукати в цьому розділі",
|
||||||
|
"selectedRelays": "Обрані ретранслятори",
|
||||||
|
"sensor": "Сенсор",
|
||||||
|
"serialError": "Помилка послідовного порту: {error}",
|
||||||
|
"setPathForContact": "Встановити шлях для {name}",
|
||||||
|
"shareLiveGps": "Поділитися GPS у реальному часі",
|
||||||
|
"shareMyTiles": "Поділитися моїми тайлами",
|
||||||
|
"sharingLocationFromPhone": "Обмін місцезнаходженням на цьому каналі з телефону.",
|
||||||
|
"sharingLocationFromRadio": "Обмін місцезнаходженням на цьому каналі з радіо.",
|
||||||
|
"signalDbm": "Сигнал {rssi} дБм",
|
||||||
|
"sizeCompressionAndPreview": "Розмір, стиснення та попередній перегляд",
|
||||||
|
"skip": "Пропустити",
|
||||||
|
"smallerFileSize": "Менший розмір файлу",
|
||||||
|
"sort": "Сортувати",
|
||||||
|
"stoppedSharingLocation": "Обмін місцезнаходженням на цьому каналі зупинено.",
|
||||||
|
"suppressNotificationsWhileInForeground": "Придушити сповіщення, коли застосунок на передньому плані",
|
||||||
|
"tabsAndNavigation": "Вкладки та навігація",
|
||||||
|
"templatesAndTutorials": "Шаблони та посібники",
|
||||||
|
"tenMinutes": "10 хвилин",
|
||||||
|
"tenSeconds": "10 секунд",
|
||||||
|
"testSendUpdate": "Тестове надсилання оновлення",
|
||||||
|
"themeLanguageAndDisplayPreferences": "Тема, мова та налаштування відображення",
|
||||||
|
"thirtyMinutes": "30 хвилин",
|
||||||
|
"thirtySeconds": "30 секунд",
|
||||||
|
"threeBytesMode2": "3 байти (режим 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "Статистика трафіку, історія повідомлень та інструменти розробника",
|
||||||
|
"twoBytesMode1": "2 байти (режим 1)",
|
||||||
|
"twoMinutes": "2 хвилини",
|
||||||
|
"undo": "Скасувати",
|
||||||
|
"updated": "Оновлено",
|
||||||
|
"useContactFlags": "Використовувати прапорці контактів",
|
||||||
|
"useSavedCoordinates": "Використати збережені координати",
|
||||||
|
"view": "Перегляд",
|
||||||
|
"viewNeighbours": "Переглянути сусідів",
|
||||||
|
"viewPublicStats": "Переглянути публічну статистику",
|
||||||
|
"setRegionScope": "Встановити область регіону",
|
||||||
|
"regionScope": "Область регіону",
|
||||||
|
"regionScopeNone": "Немає (глобально)",
|
||||||
|
"clearRegionScope": "Очистити область",
|
||||||
|
"regionScopeWarning": "Лише ретранслятори, що дозволяють цей регіон, будуть пересилати.",
|
||||||
|
"discoverRegions": "Виявити з ретрансляторів",
|
||||||
|
"discoveringRegions": "Виявлення регіонів...",
|
||||||
|
"enterRegionName": "Назва регіону (напр. auckland)",
|
||||||
|
"noRegionsFound": "Регіони не знайдені на цьому ретрансляторі.",
|
||||||
|
"regionScopeSet": "Область регіону встановлена на {name}",
|
||||||
|
"regionScopeCleared": "Область регіону очищена",
|
||||||
|
"simpleMode": "Простий режим",
|
||||||
|
"simpleModeDescription": "Лише чат і карта з обраними контактами",
|
||||||
|
"simpleModeRequiresLockedDestination": "Спочатку увімкніть «Зафіксувати призначення» з каналом, щоб використовувати простий режим",
|
||||||
|
"noFavouriteContacts": "Немає обраних контактів",
|
||||||
|
"selectDestinationFirst": "Спершу виберіть канал, контакт або кімнату",
|
||||||
|
"recipientNoLongerAvailable": "Одержувач більше недоступний",
|
||||||
|
"failedToSendMessage": "Не вдалося надіслати: {error}",
|
||||||
|
"tictactoeDirectOnly": "Хрестики-нулики працюють лише в особистих повідомленнях. Спершу виберіть контакт.",
|
||||||
|
"deviceKeyUnavailable": "Ключ пристрою недоступний",
|
||||||
|
"imageCompressionFailed": "Не вдалося стиснути зображення",
|
||||||
|
"imageFragmentationFailed": "Не вдалося розділити зображення на фрагменти",
|
||||||
|
"failedToAnnounceImage": "Не вдалося анонсувати зображення",
|
||||||
|
"noRecipientSelected": "Одержувача не вибрано",
|
||||||
|
"imageSendFailed": "Не вдалося надіслати зображення",
|
||||||
|
"microphonePermissionRequiredForVoice": "Для голосових повідомлень потрібен дозвіл на мікрофон",
|
||||||
|
"recordingDiscarded": "Запис відхилено",
|
||||||
|
"publicChannelMediaWarning": "Ви збираєтеся надіслати {mediaType} в публічний канал. Це не рекомендується: отримати це може будь-хто в mesh-мережі. Виберіть приватний або тегований канал, якщо тільки це не те, що ви хочете.",
|
||||||
|
"selectPrivateChannelToShareLocation": "Виберіть приватний канал, щоб поділитися місцезнаходженням.",
|
||||||
|
"stopSharingMyLocation": "Припинити ділитися місцезнаходженням",
|
||||||
|
"shareMyLocation": "Поділитися місцезнаходженням",
|
||||||
|
"stopRecording": "Зупинити запис",
|
||||||
|
"recordVoice": "Записати голосове повідомлення",
|
||||||
|
"moreActions": "Інші дії",
|
||||||
|
"noMessagesInFilter": "У цьому фільтрі немає повідомлень",
|
||||||
|
"noMatchesInFilter": "У цьому фільтрі немає збігів",
|
||||||
|
"syncFailed": "Помилка синхронізації: {error}",
|
||||||
|
"invalidLink": "Недійсне посилання",
|
||||||
|
"cannotOpenLink": "Не вдається відкрити посилання",
|
||||||
|
"failedToOpenLink": "Не вдалося відкрити посилання",
|
||||||
|
"failedToResendMessage": "Не вдалося повторно надіслати повідомлення",
|
||||||
|
"retryFailed": "Повторна спроба не вдалася: {error}",
|
||||||
|
"deleteDrawingAndMessage": "Видалити рисунок і повідомлення",
|
||||||
|
"lastEchoRelay": "Останнє відлуння: ретранслятор",
|
||||||
|
"lastEchoPath": "Останнє відлуння: шлях",
|
||||||
|
"lastEchoBytesReport": "Останнє відлуння: звіт про байти",
|
||||||
|
"noGpsCoordinatesAvailable": "Координати GPS недоступні",
|
||||||
|
"notASarMarker": "Це не маркер SAR",
|
||||||
|
"failedToSaveTemplate": "Не вдалося зберегти шаблон: {error}",
|
||||||
|
"drawingNotFound": "Рисунок не знайдено",
|
||||||
|
"unknownDrawingType": "Невідомий тип рисунка",
|
||||||
|
"customMapMarker": "Власний маркер карти",
|
||||||
|
"customMapPointLabel": "Точка: {point}",
|
||||||
|
"mapIdLabel": "ID карти: {id}",
|
||||||
|
"cannotFetchVoice": "Не вдається отримати голосове повідомлення",
|
||||||
|
"senderContactUnknown": "Контакт відправника невідомий. Спершу синхронізуйте контакти.",
|
||||||
|
"senderRouteUnknown": "Маршрут відправника невідомий. Спершу синхронізуйте контакти/шлях.",
|
||||||
|
"messageTooFar": "Повідомлення задалеко ({hops} хопів, макс. {maxHops}).",
|
||||||
|
"senderRouteNoPathResponse": "Маршрут відправника не відповів на перевірку шляху. Синхронізуйте контакти/шлях і спробуйте ще раз.",
|
||||||
|
"senderRouteNoRawResponse": "Маршрут відправника не відповів на raw-транспортному шляху.",
|
||||||
|
"voiceFetchOverHops": "Отримання голосу через {hops} хопів може зайняти час.",
|
||||||
|
"voiceReceiveCanceled": "Приймання голосового повідомлення скасовано",
|
||||||
|
"fetchingMissingVoiceFragments": "Отримання відсутніх фрагментів голосу",
|
||||||
|
"receivingVoice": "Приймання голосу",
|
||||||
|
"cannotFetchImage": "Не вдається отримати зображення",
|
||||||
|
"imageFetchOverHops": "Отримання зображення через {hops} хопів може зайняти час.",
|
||||||
|
"imageAlreadyBeingReceived": "Зображення вже приймається",
|
||||||
|
"loadImage": "Завантажити зображення",
|
||||||
|
"imageFetchFailedToSendRequest": "Не вдалося надіслати запит на зображення",
|
||||||
|
"imageUnavailable": "Зображення зараз недоступне",
|
||||||
|
"imageFetchTimedOut": "Час очікування зображення минув",
|
||||||
|
"imageReceiveCanceled": "Приймання зображення скасовано",
|
||||||
|
"closeImagePreview": "Закрити перегляд зображення",
|
||||||
|
"recordingReleaseToSend": "Запис... відпустіть, щоб надіслати",
|
||||||
|
"sendingVoice": "Надсилання голосу...",
|
||||||
|
"sendLongPressToRecordVoice": "Надіслати (утримуйте для запису голосу)",
|
||||||
|
"discardRecording": "Відхилити запис",
|
||||||
|
"hopCount": "{count, plural, one{{count} хоп} few{{count} хопи} many{{count} хопів} other{{count} хопа}}",
|
||||||
|
"channelsSectionDescription": "Широкомовні канали для mesh-трафіку поблизу",
|
||||||
|
"roomsSectionDescription": "Спільні простори для постійної координації команди",
|
||||||
|
"contactsSectionDescription": "Люди та пристрої в прямій досяжності",
|
||||||
|
"noRecentChatters": "Немає нещодавніх співрозмовників",
|
||||||
|
"public": "Публічний",
|
||||||
|
"failedToSendTicTacToeMove": "Не вдалося надіслати хід у хрестики-нулики",
|
||||||
|
"youWon": "Ви перемогли",
|
||||||
|
"opponentWon": "Переміг суперник",
|
||||||
|
"gameDraw": "Нічия",
|
||||||
|
"yourTurn": "Ваш хід",
|
||||||
|
"opponentTurn": "Хід суперника",
|
||||||
|
"pleaseEnterValidCoordinates": "Введіть коректні координати",
|
||||||
|
"locationNotAvailable": "Місцезнаходження недоступне",
|
||||||
|
"customCaveMapPoint": "Власна точка карти печери",
|
||||||
|
"mapPoint": "Точка карти",
|
||||||
|
"fetchingMissingImageFragments": "Отримання відсутніх фрагментів…",
|
||||||
|
"loadingImage": "Завантаження…",
|
||||||
|
"receivingImage": "Приймання…",
|
||||||
|
"tapToLoad": "Торкніться, щоб завантажити",
|
||||||
|
"connectDevice": "Підключити пристрій",
|
||||||
|
"chooseTransportSubtitle": "Виберіть транспорт: Bluetooth, WiFi або послідовний порт",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "Мережа",
|
||||||
|
"serial": "Послідовний порт",
|
||||||
|
"scan": "Сканувати",
|
||||||
|
"pressScanToSearchForDevices": "Натисніть «Сканувати», щоб знайти пристрої поблизу",
|
||||||
|
"failedToConnectToDevice": "Не вдалося підключитися до {name}",
|
||||||
|
"unknownDevice": "Невідомий пристрій",
|
||||||
|
"showingCachedResultsTapRefresh": "Показано кешовані результати. Натисніть «Оновити», щоб пересканувати.",
|
||||||
|
"scanningLocalNetworkOnPort": "Сканування локальної мережі на наявність WiFi-пристроїв MeshCore на порту {port}",
|
||||||
|
"cancelScanAndAddServer": "Скасувати сканування та додати сервер",
|
||||||
|
"addServer": "Додати сервер",
|
||||||
|
"scanningProgressIps": "Сканування... {scanned}/{total} IP-адрес",
|
||||||
|
"cancelAndEnterManually": "Скасувати та ввести вручну",
|
||||||
|
"noRecentOrDiscoveredServers": "Поки немає нещодавніх або виявлених серверів",
|
||||||
|
"recentlyUsed": "Нещодавно використані",
|
||||||
|
"connecting": "Підключення...",
|
||||||
|
"discoveredOnThisNetwork": "Виявлені в цій мережі",
|
||||||
|
"serverNoLongerAvailable": "Сервер {host}:{port} більше недоступний. Виконайте сканування ще раз, щоб знайти активні сервери.",
|
||||||
|
"failedToConnectToHost": "Не вдалося підключитися до {host}:{port}",
|
||||||
|
"enterValidIpAddress": "Введіть коректну IP-адресу",
|
||||||
|
"enterValidTcpPort": "Введіть коректний TCP-порт",
|
||||||
|
"ipAddress": "IP-адреса",
|
||||||
|
"tcpPort": "TCP-порт",
|
||||||
|
"customServerPort": "Власний порт сервера",
|
||||||
|
"chat": "Чат",
|
||||||
|
"restoringPreviousLink": "Відновлення попереднього підключення",
|
||||||
|
"noDeviceConnected": "Немає підключеного пристрою",
|
||||||
|
"sendAdvert": "Надіслати мій контакт",
|
||||||
|
"advertMode": "Надіслати мій контакт",
|
||||||
|
"advertModeSubtitle": "Виберіть, як далеко мають поширитися ваші контактні дані.",
|
||||||
|
"floodAdvertSent": "Контакт надіслано по всій mesh-мережі",
|
||||||
|
"directAdvertSent": "Контакт надіслано пристроям поблизу"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,10 +36,6 @@
|
|||||||
"@scanAgain": {
|
"@scanAgain": {
|
||||||
"description": "重新开始 BLE 扫描的按钮"
|
"description": "重新开始 BLE 扫描的按钮"
|
||||||
},
|
},
|
||||||
"tapToConnect": "点击连接",
|
|
||||||
"@tapToConnect": {
|
|
||||||
"description": "扫描列表中设备的副标题文本"
|
|
||||||
},
|
|
||||||
"deviceNotConnected": "设备未连接",
|
"deviceNotConnected": "设备未连接",
|
||||||
"@deviceNotConnected": {
|
"@deviceNotConnected": {
|
||||||
"description": "设备未连接时的错误消息"
|
"description": "设备未连接时的错误消息"
|
||||||
@@ -133,18 +129,10 @@
|
|||||||
"@showRxTxIndicators": {
|
"@showRxTxIndicators": {
|
||||||
"description": "显示 RX/TX 指示器的设置"
|
"description": "显示 RX/TX 指示器的设置"
|
||||||
},
|
},
|
||||||
"displayPacketActivity": "在顶部栏显示数据包活动指示器",
|
|
||||||
"@displayPacketActivity": {
|
|
||||||
"description": "RX/TX 指示器设置的描述"
|
|
||||||
},
|
|
||||||
"disableMap": "禁用地图",
|
"disableMap": "禁用地图",
|
||||||
"@disableMap": {
|
"@disableMap": {
|
||||||
"description": "禁用地图标签页的设置"
|
"description": "禁用地图标签页的设置"
|
||||||
},
|
},
|
||||||
"disableMapDescription": "隐藏地图标签页以减少电池消耗",
|
|
||||||
"@disableMapDescription": {
|
|
||||||
"description": "禁用地图设置的描述"
|
|
||||||
},
|
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"@language": {
|
"@language": {
|
||||||
"description": "语言设置标签"
|
"description": "语言设置标签"
|
||||||
@@ -173,10 +161,6 @@
|
|||||||
"@appVersion": {
|
"@appVersion": {
|
||||||
"description": "应用版本标签"
|
"description": "应用版本标签"
|
||||||
},
|
},
|
||||||
"appName": "应用名称",
|
|
||||||
"@appName": {
|
|
||||||
"description": "应用名称标签"
|
|
||||||
},
|
|
||||||
"aboutMeshCoreSar": "关于 MeshCore 搜救助手",
|
"aboutMeshCoreSar": "关于 MeshCore 搜救助手",
|
||||||
"@aboutMeshCoreSar": {
|
"@aboutMeshCoreSar": {
|
||||||
"description": "关于对话框标题"
|
"description": "关于对话框标题"
|
||||||
@@ -308,10 +292,6 @@
|
|||||||
"@resetPath": {
|
"@resetPath": {
|
||||||
"description": "重置联系人路径以重新路由的操作"
|
"description": "重置联系人路径以重新路由的操作"
|
||||||
},
|
},
|
||||||
"publicKeyCopied": "公钥已复制到剪贴板",
|
|
||||||
"@publicKeyCopied": {
|
|
||||||
"description": "复制公钥后的成功消息"
|
|
||||||
},
|
|
||||||
"copiedToClipboard": "{label} 已复制到剪贴板",
|
"copiedToClipboard": "{label} 已复制到剪贴板",
|
||||||
"@copiedToClipboard": {
|
"@copiedToClipboard": {
|
||||||
"description": "将值复制到剪贴板后的成功消息",
|
"description": "将值复制到剪贴板后的成功消息",
|
||||||
@@ -720,22 +700,6 @@
|
|||||||
"@lastSeen": {
|
"@lastSeen": {
|
||||||
"description": "最后在线标签"
|
"description": "最后在线标签"
|
||||||
},
|
},
|
||||||
"roomStatus": "房间状态",
|
|
||||||
"@roomStatus": {
|
|
||||||
"description": "房间状态区域标题"
|
|
||||||
},
|
|
||||||
"loginStatus": "登录状态",
|
|
||||||
"@loginStatus": {
|
|
||||||
"description": "登录状态标签"
|
|
||||||
},
|
|
||||||
"notLoggedIn": "未登录",
|
|
||||||
"@notLoggedIn": {
|
|
||||||
"description": "未登录状态"
|
|
||||||
},
|
|
||||||
"adminAccess": "管理员权限",
|
|
||||||
"@adminAccess": {
|
|
||||||
"description": "管理员权限标签"
|
|
||||||
},
|
|
||||||
"yes": "是",
|
"yes": "是",
|
||||||
"@yes": {
|
"@yes": {
|
||||||
"description": "肯定回答"
|
"description": "肯定回答"
|
||||||
@@ -748,14 +712,6 @@
|
|||||||
"@permissions": {
|
"@permissions": {
|
||||||
"description": "权限标签"
|
"description": "权限标签"
|
||||||
},
|
},
|
||||||
"passwordSaved": "密码已保存",
|
|
||||||
"@passwordSaved": {
|
|
||||||
"description": "密码已保存标签"
|
|
||||||
},
|
|
||||||
"locationColon": "位置:",
|
|
||||||
"@locationColon": {
|
|
||||||
"description": "位置区域标题"
|
|
||||||
},
|
|
||||||
"telemetry": "遥测",
|
"telemetry": "遥测",
|
||||||
"@telemetry": {
|
"@telemetry": {
|
||||||
"description": "遥测区域标题"
|
"description": "遥测区域标题"
|
||||||
@@ -784,19 +740,6 @@
|
|||||||
"@gpsTelemetry": {
|
"@gpsTelemetry": {
|
||||||
"description": "来自遥测的 GPS 标签"
|
"description": "来自遥测的 GPS 标签"
|
||||||
},
|
},
|
||||||
"updated": "更新于",
|
|
||||||
"@updated": {
|
|
||||||
"description": "更新时间戳标签"
|
|
||||||
},
|
|
||||||
"pathResetInfo": "已重置 {name} 的路径。下一条消息将寻找新路由。",
|
|
||||||
"@pathResetInfo": {
|
|
||||||
"description": "路径重置后的信息消息",
|
|
||||||
"placeholders": {
|
|
||||||
"name": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"reLoginToRoom": "重新登录到房间",
|
"reLoginToRoom": "重新登录到房间",
|
||||||
"@reLoginToRoom": {
|
"@reLoginToRoom": {
|
||||||
"description": "重新登录到房间的按钮"
|
"description": "重新登录到房间的按钮"
|
||||||
@@ -1158,10 +1101,6 @@
|
|||||||
"@frequencyMHz": {
|
"@frequencyMHz": {
|
||||||
"description": "无线电频率字段标签"
|
"description": "无线电频率字段标签"
|
||||||
},
|
},
|
||||||
"frequencyExample": "例如 869.618",
|
|
||||||
"@frequencyExample": {
|
|
||||||
"description": "频率示例帮助文本"
|
|
||||||
},
|
|
||||||
"bandwidth": "带宽",
|
"bandwidth": "带宽",
|
||||||
"@bandwidth": {
|
"@bandwidth": {
|
||||||
"description": "带宽下拉菜单标签"
|
"description": "带宽下拉菜单标签"
|
||||||
@@ -1265,47 +1204,6 @@
|
|||||||
"@trailControls": {
|
"@trailControls": {
|
||||||
"description": "轨迹控制工具提示"
|
"description": "轨迹控制工具提示"
|
||||||
},
|
},
|
||||||
"contactTrails": "联系人轨迹",
|
|
||||||
"@contactTrails": {
|
|
||||||
"description": "联系人轨迹区域标题"
|
|
||||||
},
|
|
||||||
"showAllContactTrails": "显示所有联系人轨迹",
|
|
||||||
"@showAllContactTrails": {
|
|
||||||
"description": "显示所有联系人轨迹的开关标签"
|
|
||||||
},
|
|
||||||
"noContactsWithLocationHistory": "没有带位置历史的联系人",
|
|
||||||
"@noContactsWithLocationHistory": {
|
|
||||||
"description": "当没有联系人拥有轨迹时的副标题"
|
|
||||||
},
|
|
||||||
"showingTrailsForContacts": "正在显示 {count} 个联系人的轨迹",
|
|
||||||
"@showingTrailsForContacts": {
|
|
||||||
"description": "显示拥有轨迹的联系人数量的副标题",
|
|
||||||
"placeholders": {
|
|
||||||
"count": {
|
|
||||||
"type": "int"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"individualContactTrails": "单个联系人轨迹",
|
|
||||||
"@individualContactTrails": {
|
|
||||||
"description": "单个联系人轨迹的扩展磁贴标题"
|
|
||||||
},
|
|
||||||
"deviceInformation": "设备信息",
|
|
||||||
"@deviceInformation": {
|
|
||||||
"description": "设备信息区域标题"
|
|
||||||
},
|
|
||||||
"bleName": "BLE 名称",
|
|
||||||
"@bleName": {
|
|
||||||
"description": "低功耗蓝牙设备名称标签"
|
|
||||||
},
|
|
||||||
"meshName": "Mesh 名称",
|
|
||||||
"@meshName": {
|
|
||||||
"description": "Mesh 网络名称标签"
|
|
||||||
},
|
|
||||||
"notSet": "未设置",
|
|
||||||
"@notSet": {
|
|
||||||
"description": "当值未设置时的标签"
|
|
||||||
},
|
|
||||||
"model": "型号",
|
"model": "型号",
|
||||||
"@model": {
|
"@model": {
|
||||||
"description": "设备型号标签"
|
"description": "设备型号标签"
|
||||||
@@ -1314,14 +1212,6 @@
|
|||||||
"@version": {
|
"@version": {
|
||||||
"description": "版本标签"
|
"description": "版本标签"
|
||||||
},
|
},
|
||||||
"buildDate": "构建日期",
|
|
||||||
"@buildDate": {
|
|
||||||
"description": "固件构建日期标签"
|
|
||||||
},
|
|
||||||
"firmware": "固件",
|
|
||||||
"@firmware": {
|
|
||||||
"description": "固件标签"
|
|
||||||
},
|
|
||||||
"maxContacts": "最大联系人数量",
|
"maxContacts": "最大联系人数量",
|
||||||
"@maxContacts": {
|
"@maxContacts": {
|
||||||
"description": "最大联系人容量标签"
|
"description": "最大联系人容量标签"
|
||||||
@@ -1334,26 +1224,6 @@
|
|||||||
"@publicInfo": {
|
"@publicInfo": {
|
||||||
"description": "公共信息区域标题"
|
"description": "公共信息区域标题"
|
||||||
},
|
},
|
||||||
"meshNetworkName": "Mesh 网络名称",
|
|
||||||
"@meshNetworkName": {
|
|
||||||
"description": "Mesh 网络名称字段标签"
|
|
||||||
},
|
|
||||||
"nameBroadcastInMesh": "在 Mesh 广播中公布的名称",
|
|
||||||
"@nameBroadcastInMesh": {
|
|
||||||
"description": "Mesh 网络名称字段的帮助文本"
|
|
||||||
},
|
|
||||||
"telemetryAndLocationSharing": "遥测与位置共享",
|
|
||||||
"@telemetryAndLocationSharing": {
|
|
||||||
"description": "遥测和位置共享开关标签"
|
|
||||||
},
|
|
||||||
"lat": "纬度",
|
|
||||||
"@lat": {
|
|
||||||
"description": "纬度字段标签(短格式)"
|
|
||||||
},
|
|
||||||
"lon": "经度",
|
|
||||||
"@lon": {
|
|
||||||
"description": "经度字段标签(短格式)"
|
|
||||||
},
|
|
||||||
"useCurrentLocation": "使用当前位置",
|
"useCurrentLocation": "使用当前位置",
|
||||||
"@useCurrentLocation": {
|
"@useCurrentLocation": {
|
||||||
"description": "使用当前位置按钮的工具提示"
|
"description": "使用当前位置按钮的工具提示"
|
||||||
@@ -1383,15 +1253,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"copiedToClipboardShort": "{label} 已复制到剪贴板",
|
|
||||||
"@copiedToClipboardShort": {
|
|
||||||
"description": "复制到剪贴板的简短成功消息",
|
|
||||||
"placeholders": {
|
|
||||||
"label": {
|
|
||||||
"type": "String"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"failedToSave": "保存失败:{error}",
|
"failedToSave": "保存失败:{error}",
|
||||||
"@failedToSave": {
|
"@failedToSave": {
|
||||||
"description": "保存失败的通用错误消息",
|
"description": "保存失败的通用错误消息",
|
||||||
@@ -1414,10 +1275,6 @@
|
|||||||
"@sarTemplates": {
|
"@sarTemplates": {
|
||||||
"description": "搜救模板菜单标题"
|
"description": "搜救模板菜单标题"
|
||||||
},
|
},
|
||||||
"manageSarTemplates": "管理光标目标模板",
|
|
||||||
"@manageSarTemplates": {
|
|
||||||
"description": "搜救模板设置的副标题"
|
|
||||||
},
|
|
||||||
"addTemplate": "添加模板",
|
"addTemplate": "添加模板",
|
||||||
"@addTemplate": {
|
"@addTemplate": {
|
||||||
"description": "添加新搜救模板的按钮"
|
"description": "添加新搜救模板的按钮"
|
||||||
@@ -1541,10 +1398,6 @@
|
|||||||
"@ok": {
|
"@ok": {
|
||||||
"description": "确定按钮标签"
|
"description": "确定按钮标签"
|
||||||
},
|
},
|
||||||
"permissionsSection": "权限",
|
|
||||||
"@permissionsSection": {
|
|
||||||
"description": "权限区域标题"
|
|
||||||
},
|
|
||||||
"locationPermission": "位置权限",
|
"locationPermission": "位置权限",
|
||||||
"@locationPermission": {
|
"@locationPermission": {
|
||||||
"description": "位置权限标签"
|
"description": "位置权限标签"
|
||||||
@@ -2109,94 +1962,6 @@
|
|||||||
"@wizardWelcomeTitle": {
|
"@wizardWelcomeTitle": {
|
||||||
"description": "欢迎向导第一页标题"
|
"description": "欢迎向导第一页标题"
|
||||||
},
|
},
|
||||||
"wizardWelcomeDescription": "一个功能强大的离网通信工具,用于搜索和救援行动。当传统网络不可用时,使用 Mesh 无线电技术与您的团队保持联系。",
|
|
||||||
"@wizardWelcomeDescription": {
|
|
||||||
"description": "欢迎向导第一页描述"
|
|
||||||
},
|
|
||||||
"wizardConnectingTitle": "连接到您的无线电",
|
|
||||||
"@wizardConnectingTitle": {
|
|
||||||
"description": "向导连接页面标题"
|
|
||||||
},
|
|
||||||
"wizardConnectingDescription": "通过蓝牙将您的智能手机连接到 MeshCore 无线电设备,开始离网通信。",
|
|
||||||
"@wizardConnectingDescription": {
|
|
||||||
"description": "向导连接页面描述"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature1": "扫描附近的 MeshCore 设备",
|
|
||||||
"@wizardConnectingFeature1": {
|
|
||||||
"description": "向导连接功能 1"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature2": "通过蓝牙与您的无线电配对",
|
|
||||||
"@wizardConnectingFeature2": {
|
|
||||||
"description": "向导连接功能 2"
|
|
||||||
},
|
|
||||||
"wizardConnectingFeature3": "完全离线工作 - 无需互联网",
|
|
||||||
"@wizardConnectingFeature3": {
|
|
||||||
"description": "向导连接功能 3"
|
|
||||||
},
|
|
||||||
"wizardChannelTitle": "频道",
|
|
||||||
"@wizardChannelTitle": {
|
|
||||||
"description": "向导频道页面标题"
|
|
||||||
},
|
|
||||||
"wizardChannelDescription": "向频道上的所有人广播消息,非常适合全队通告和协调。",
|
|
||||||
"@wizardChannelDescription": {
|
|
||||||
"description": "向导频道页面描述"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature1": "用于一般团队通信的公共频道",
|
|
||||||
"@wizardChannelFeature1": {
|
|
||||||
"description": "向导频道功能 1"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature2": "为特定小组创建自定义频道",
|
|
||||||
"@wizardChannelFeature2": {
|
|
||||||
"description": "向导频道功能 2"
|
|
||||||
},
|
|
||||||
"wizardChannelFeature3": "消息由 Mesh 网络自动转发",
|
|
||||||
"@wizardChannelFeature3": {
|
|
||||||
"description": "向导频道功能 3"
|
|
||||||
},
|
|
||||||
"wizardContactsTitle": "联系人",
|
|
||||||
"@wizardContactsTitle": {
|
|
||||||
"description": "向导联系人页面标题"
|
|
||||||
},
|
|
||||||
"wizardContactsDescription": "您的团队成员在加入 Mesh 网络时会自动出现。向他们发送直连消息或查看他们的位置。",
|
|
||||||
"@wizardContactsDescription": {
|
|
||||||
"description": "向导联系人页面描述"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature1": "联系人自动发现",
|
|
||||||
"@wizardContactsFeature1": {
|
|
||||||
"description": "向导联系人功能 1"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature2": "发送私密的直连消息",
|
|
||||||
"@wizardContactsFeature2": {
|
|
||||||
"description": "向导联系人功能 2"
|
|
||||||
},
|
|
||||||
"wizardContactsFeature3": "查看电池电量和最后在线时间",
|
|
||||||
"@wizardContactsFeature3": {
|
|
||||||
"description": "向导联系人功能 3"
|
|
||||||
},
|
|
||||||
"wizardMapTitle": "地图与位置",
|
|
||||||
"@wizardMapTitle": {
|
|
||||||
"description": "向导地图页面标题"
|
|
||||||
},
|
|
||||||
"wizardMapDescription": "实时跟踪您的团队,并为搜救行动标记重要位置。",
|
|
||||||
"@wizardMapDescription": {
|
|
||||||
"description": "向导地图页面描述"
|
|
||||||
},
|
|
||||||
"wizardMapFeature1": "用于标记找到的人员、火灾和集结区的搜救标记",
|
|
||||||
"@wizardMapFeature1": {
|
|
||||||
"description": "向导地图功能 1"
|
|
||||||
},
|
|
||||||
"wizardMapFeature2": "团队成员的实时 GPS 跟踪",
|
|
||||||
"@wizardMapFeature2": {
|
|
||||||
"description": "向导地图功能 2"
|
|
||||||
},
|
|
||||||
"wizardMapFeature3": "为偏远地区下载离线地图",
|
|
||||||
"@wizardMapFeature3": {
|
|
||||||
"description": "向导地图功能 3"
|
|
||||||
},
|
|
||||||
"wizardMapFeature4": "绘制形状并共享战术信息",
|
|
||||||
"@wizardMapFeature4": {
|
|
||||||
"description": "向导地图功能 4"
|
|
||||||
},
|
|
||||||
"viewWelcomeTutorial": "查看欢迎教程",
|
"viewWelcomeTutorial": "查看欢迎教程",
|
||||||
"@viewWelcomeTutorial": {
|
"@viewWelcomeTutorial": {
|
||||||
"description": "设置中重新显示欢迎向导的选项"
|
"description": "设置中重新显示欢迎向导的选项"
|
||||||
@@ -2228,80 +1993,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 +2093,525 @@
|
|||||||
"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": "带通语音滤波",
|
||||||
|
"active": "活动",
|
||||||
|
"addContact": "添加联系人",
|
||||||
|
"all": "全部",
|
||||||
|
"clearAllLabel": "清除全部",
|
||||||
|
"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": "分享",
|
||||||
|
"trace": "追踪",
|
||||||
|
"discovery": "发现",
|
||||||
|
"discoverRepeaters": "发现中继器",
|
||||||
|
"discoverSensors": "发现传感器",
|
||||||
|
"repeaterDiscoverySent": "中继器发现已发送",
|
||||||
|
"sensorDiscoverySent": "传感器发现已发送",
|
||||||
|
"clearedPendingDiscoveries": "已清除待处理的发现。",
|
||||||
|
"autoDiscovery": "自动发现",
|
||||||
|
"enableAutomaticAdding": "启用自动添加",
|
||||||
|
"autoaddRepeaters": "自动添加中继器",
|
||||||
|
"autoaddRoomServers": "自动添加房间服务器",
|
||||||
|
"autoaddSensors": "自动添加传感器",
|
||||||
|
"autoaddUsers": "自动添加用户",
|
||||||
|
"overwriteOldestWhenFull": "满时覆盖最旧的",
|
||||||
|
"storage": "存储",
|
||||||
|
"dangerZone": "危险区域",
|
||||||
|
"profiles": "配置文件",
|
||||||
|
"favourites": "收藏夹",
|
||||||
|
"sensors": "传感器",
|
||||||
|
"others": "其他",
|
||||||
|
"gpsModule": "GPS 模块",
|
||||||
|
"liveTraffic": "实时流量",
|
||||||
|
"repeatersMap": "中继器地图",
|
||||||
|
"blePacketLogs": "BLE 数据包日志",
|
||||||
|
"onlineTraceDatabase": "在线追踪数据库",
|
||||||
|
"routePathByteSize": "路由路径字节大小",
|
||||||
|
"messageNotifications": "消息通知",
|
||||||
|
"sarAlerts": "SAR 警报",
|
||||||
|
"discoveryNotifications": "发现通知",
|
||||||
|
"updateNotifications": "更新通知",
|
||||||
|
"muteWhileAppIsOpen": "应用打开时静音",
|
||||||
|
"disableContacts": "禁用联系人",
|
||||||
|
"enableSensorsTab": "启用传感器标签",
|
||||||
|
"enableProfiles": "启用配置文件",
|
||||||
|
"nearestRepeaterFallback": "最近中继器回退",
|
||||||
|
"deleteAllStoredMessageHistory": "删除所有存储的消息历史",
|
||||||
|
"messageFontSize": "消息字体大小",
|
||||||
|
"rotateMapWithHeading": "随航向旋转地图",
|
||||||
|
"showMapDebugInfo": "显示地图调试信息",
|
||||||
|
"openMapInFullscreen": "全屏打开地图",
|
||||||
|
"showSarMarkersLabel": "显示 SAR 标记",
|
||||||
|
"hideRepeatersOnMap": "在地图上隐藏中继器",
|
||||||
|
"setMapScale": "设置地图比例",
|
||||||
|
"customMapScaleSaved": "自定义地图比例已保存",
|
||||||
|
"voiceBitrate": "语音比特率",
|
||||||
|
"voiceCompressor": "语音压缩器",
|
||||||
|
"voiceLimiter": "语音限制器",
|
||||||
|
"micAutoGain": "麦克风自动增益",
|
||||||
|
"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": "安静",
|
||||||
|
"busy": "繁忙",
|
||||||
|
"searchMessages": "搜索消息",
|
||||||
|
"sendImageFromGallery": "从图库发送图片",
|
||||||
|
"takePhoto": "拍照",
|
||||||
|
"allMessages": "所有消息",
|
||||||
|
"sendToPublicChannel": "发送到公共频道?",
|
||||||
|
"selectMarkerTypeAndDestination": "选择标记类型和目标",
|
||||||
|
"noDestinationsAvailableLabel": "没有可用的目标",
|
||||||
|
"image": "图片",
|
||||||
|
"format": "格式",
|
||||||
|
"dimensions": "尺寸",
|
||||||
|
"segments": "分段",
|
||||||
|
"transfers": "传输",
|
||||||
|
"downloadedBy": "已下载",
|
||||||
|
"saveDiscoverySettings": "保存发现设置",
|
||||||
|
"savePublicInfo": "保存公共信息",
|
||||||
|
"saveRadioSettings": "保存无线电设置",
|
||||||
|
"savePath": "保存路径",
|
||||||
|
"wipeDeviceData": "擦除设备数据",
|
||||||
|
"wipeDevice": "擦除设备",
|
||||||
|
"destructiveDeviceActions": "破坏性设备操作。",
|
||||||
|
"chooseAPresetOrFinetuneCustomRadioSettings": "选择预设或微调自定义无线电设置。",
|
||||||
|
"used": "已使用",
|
||||||
|
"total": "总计",
|
||||||
|
"renameValue": "重命名值",
|
||||||
|
"customizeFields": "自定义字段",
|
||||||
|
"livePreview": "实时预览",
|
||||||
|
"refreshSchedule": "刷新计划",
|
||||||
|
"noResponse": "无响应",
|
||||||
|
"refreshing": "刷新中",
|
||||||
|
"unavailable": "不可用",
|
||||||
|
"pickARelayOrNodeToWatchInSensors": "选择要在传感器中监视的中继或节点。",
|
||||||
|
"publicKeyLabel": "公钥",
|
||||||
|
"alreadyInContacts": "已在联系人中",
|
||||||
|
"connectToADeviceBeforeAddingContacts": "添加联系人前请连接到设备",
|
||||||
|
"fromContacts": "来自联系人",
|
||||||
|
"onlineOnly": "仅在线",
|
||||||
|
"inBoth": "两者中",
|
||||||
|
"source": "来源",
|
||||||
|
"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": "没有设备联系人可清除。",
|
||||||
|
"autoSend": "自动发送",
|
||||||
|
"autoSendDescription": "通过当前路径发送。",
|
||||||
|
"sendDirect": "直接发送",
|
||||||
|
"sendDirectDescription": "直接发送给此联系人。",
|
||||||
|
"sendFlood": "泛洪发送",
|
||||||
|
"sendFloodDescription": "通过所有中继器发送。",
|
||||||
|
"postConnectDiscoveryTitle": "现在发现中继器?",
|
||||||
|
"postConnectDiscoveryDescription": "连接后立即运行中继器发现,以查看附近的 MeshCore 节点并更快地将它们添加到您的网络中。",
|
||||||
|
"aToZ": "A-Z",
|
||||||
|
"add": "添加",
|
||||||
|
"addAdditionalDetails": "添加其他详情",
|
||||||
|
"addPeer": "添加节点",
|
||||||
|
"addPeerManually": "手动添加节点",
|
||||||
|
"addedToContacts": "{name} 已添加到联系人",
|
||||||
|
"addedToSensors": "{name} 已添加到传感器",
|
||||||
|
"allowAll": "全部允许",
|
||||||
|
"anonymousRxStats": "匿名接收统计",
|
||||||
|
"appearance": "外观",
|
||||||
|
"ascii": "ASCII",
|
||||||
|
"baseTelemetry": "基础遥测",
|
||||||
|
"blePin": "BLE PIN",
|
||||||
|
"buzzerAlerts": "蜂鸣器警报",
|
||||||
|
"cancelImageReceive": "取消接收图片",
|
||||||
|
"choose": "选择",
|
||||||
|
"chooseWhichTabsAndContactSectionsToShow": "选择要显示的标签页和联系人分区",
|
||||||
|
"clearCache": "清除缓存",
|
||||||
|
"clearHistory": "清除历史",
|
||||||
|
"clearLiveView": "清除实时视图",
|
||||||
|
"clearLogs": "清除日志",
|
||||||
|
"clearOfflineCache": "清除离线缓存?",
|
||||||
|
"clearOverlay": "清除叠加层",
|
||||||
|
"clearRouteOnlyAfterAllRetriesFail": "仅在所有重试失败后清除路由",
|
||||||
|
"codecBitrateAndAudioProcessing": "编解码器、比特率和音频处理",
|
||||||
|
"connectByIpAddress": "通过 IP 地址连接",
|
||||||
|
"controlWhichAlertsYouReceive": "控制接收哪些警报",
|
||||||
|
"copyFullHex": "复制完整 Hex",
|
||||||
|
"copyRawResponse": "复制原始响应",
|
||||||
|
"couldNotLoadMetHistory": "无法加载 MET 历史",
|
||||||
|
"customizeSensor": "自定义 {name}",
|
||||||
|
"data": "数据",
|
||||||
|
"deleteGroup": "删除群组",
|
||||||
|
"deleteStyleConfirm": "删除 {name}?",
|
||||||
|
"deleteThisDrawing": "删除此{type}?",
|
||||||
|
"deny": "拒绝",
|
||||||
|
"deviceInfo": "设备信息",
|
||||||
|
"deviceSettings": "设备设置",
|
||||||
|
"deviceTimeSynced": "设备时间已同步到此手机。",
|
||||||
|
"disableFastGpsPublishing": "禁用快速 GPS 发布",
|
||||||
|
"displayMarkersAndTraceDatabase": "显示、标记和轨迹数据库",
|
||||||
|
"download": "下载",
|
||||||
|
"environmentalTelemetry": "环境遥测",
|
||||||
|
"errorCheckingForUpdates": "检查更新出错:{error}",
|
||||||
|
"errorGeneric": "错误:{error}",
|
||||||
|
"errorOpeningDownload": "打开下载出错:{error}",
|
||||||
|
"exportLogs": "导出日志",
|
||||||
|
"extraAggressiveAvifCompression": "超强 AVIF 压缩",
|
||||||
|
"failedToAddContact": "添加联系人失败:{error}",
|
||||||
|
"failedToAddName": "添加 {name} 失败:{error}",
|
||||||
|
"failedToClearChannels": "清除频道失败:{error}",
|
||||||
|
"failedToClearContacts": "清除联系人失败:{error}",
|
||||||
|
"failedToConnectViaSerial": "通过串口连接失败",
|
||||||
|
"failedToLoadPreviewImage": "加载预览图片失败:{error}",
|
||||||
|
"failedToLoadTrace": "加载轨迹失败:{error}",
|
||||||
|
"failedToSendSarMarker": "发送 SAR 标记失败:{error}",
|
||||||
|
"failedToSetBuzzerMode": "设置蜂鸣器模式失败:{error}",
|
||||||
|
"failedToSetGpsMode": "设置 GPS 模式失败:{error}",
|
||||||
|
"failedToSyncDeviceTime": "同步设备时间失败:{error}",
|
||||||
|
"failedToWipeDeviceData": "擦除设备数据失败:{error}",
|
||||||
|
"fastGpsTargetChannel": "快速 GPS 目标频道",
|
||||||
|
"fastUpdatesThresholdsAndPermissions": "快速更新、阈值和权限",
|
||||||
|
"fetchingCatalog": "正在获取目录…",
|
||||||
|
"fifteenMinutes": "15 分钟",
|
||||||
|
"fifteenSeconds": "15 秒",
|
||||||
|
"filterByDirection": "按方向筛选",
|
||||||
|
"finish": "完成",
|
||||||
|
"fiveMinutes": "5 分钟",
|
||||||
|
"fiveSeconds": "5 秒",
|
||||||
|
"fix": "定位",
|
||||||
|
"gps": "GPS",
|
||||||
|
"gpsAdvertPolicy": "GPS 广播策略",
|
||||||
|
"gpsAndLocation": "GPS 与位置",
|
||||||
|
"gpsDiagnostics": "GPS 诊断",
|
||||||
|
"gpsSection": "GPS",
|
||||||
|
"help": "帮助",
|
||||||
|
"hex": "Hex",
|
||||||
|
"hidden": "已隐藏",
|
||||||
|
"howTheRadioAutoAddsDiscoveredNodes": "无线电自动添加已发现节点的方式。",
|
||||||
|
"images": "图片",
|
||||||
|
"importProfile": "导入配置文件",
|
||||||
|
"lastFix": "最后定位",
|
||||||
|
"latest": "最新",
|
||||||
|
"latitude": "纬度",
|
||||||
|
"loadASavedRegion": "加载已保存的区域",
|
||||||
|
"loading": "加载中",
|
||||||
|
"locationSharingHardwareAndUpdateInterval": "位置共享、硬件和更新间隔。",
|
||||||
|
"locationTelemetry": "位置遥测",
|
||||||
|
"lockDestination": "锁定目的地",
|
||||||
|
"longitude": "经度",
|
||||||
|
"mapStyle": "地图样式",
|
||||||
|
"max": "最大",
|
||||||
|
"maxZoom": "最大缩放",
|
||||||
|
"messaging": "消息传递",
|
||||||
|
"metHistory": "MET 历史",
|
||||||
|
"min": "最小",
|
||||||
|
"minZoom": "最小缩放",
|
||||||
|
"moveDown": "下移",
|
||||||
|
"moveUp": "上移",
|
||||||
|
"multiAckMode": "多重 ACK 模式",
|
||||||
|
"multiDeviceWorkspaceManagement": "多设备工作区管理",
|
||||||
|
"myLocation": "我的位置",
|
||||||
|
"nameAndTelemetryShared": "名称和遥测数据已与其他设备共享。",
|
||||||
|
"newVersionAvailable": "新版本:{version}",
|
||||||
|
"newer": "较新",
|
||||||
|
"noNeighboursFound": "未找到邻近节点",
|
||||||
|
"noServersFound": "未找到服务器",
|
||||||
|
"notSet": "未设置",
|
||||||
|
"notifications": "通知",
|
||||||
|
"offZeroSeconds": "关闭 (0 秒)",
|
||||||
|
"offlineMaps": "离线地图",
|
||||||
|
"older": "较旧",
|
||||||
|
"oneByteMode0": "1 字节(模式 0)",
|
||||||
|
"oneHour": "1 小时",
|
||||||
|
"oneMinute": "1 分钟",
|
||||||
|
"openPacketLogs": "打开数据包日志",
|
||||||
|
"packetTypeHelp": "数据包类型帮助",
|
||||||
|
"pasteShareLinkOrHexAdvert": "粘贴分享链接或 Hex 广播",
|
||||||
|
"pathHash": "路径哈希",
|
||||||
|
"pathSize": "路径大小",
|
||||||
|
"ping": "Ping",
|
||||||
|
"polygon": "多边形",
|
||||||
|
"rawResponseCopied": "原始响应已复制",
|
||||||
|
"resendThroughNearestRepeaterOnFailure": "失败时通过最近的中继器重新发送",
|
||||||
|
"resolveContact": "解析联系人",
|
||||||
|
"routingRetriesAndDestinationLock": "路由、重试和目的地锁定",
|
||||||
|
"samples": "采样",
|
||||||
|
"satellites": "卫星",
|
||||||
|
"saving": "保存中…",
|
||||||
|
"search": "搜索",
|
||||||
|
"searchByNameOrToken": "按名称或令牌搜索",
|
||||||
|
"searchDiscoveredNodes": "搜索已发现的节点",
|
||||||
|
"searchInCurrentFilter": "在当前筛选中搜索",
|
||||||
|
"searchLogs": "搜索日志…",
|
||||||
|
"searchSensors": "搜索传感器",
|
||||||
|
"searchThisSection": "搜索此分区",
|
||||||
|
"selectedRelays": "已选中继器",
|
||||||
|
"sensor": "传感器",
|
||||||
|
"serialError": "串口错误:{error}",
|
||||||
|
"setPathForContact": "为 {name} 设置路径",
|
||||||
|
"shareLiveGps": "共享实时 GPS",
|
||||||
|
"shareMyTiles": "共享我的地图瓦片",
|
||||||
|
"sharingLocationFromPhone": "正在从手机共享此频道的位置。",
|
||||||
|
"sharingLocationFromRadio": "正在从无线电共享此频道的位置。",
|
||||||
|
"signalDbm": "信号 {rssi} dBm",
|
||||||
|
"sizeCompressionAndPreview": "大小、压缩和预览",
|
||||||
|
"skip": "跳过",
|
||||||
|
"smallerFileSize": "更小的文件大小",
|
||||||
|
"sort": "排序",
|
||||||
|
"stoppedSharingLocation": "已停止在此频道上共享位置。",
|
||||||
|
"suppressNotificationsWhileInForeground": "前台运行时抑制通知",
|
||||||
|
"tabsAndNavigation": "标签页与导航",
|
||||||
|
"templatesAndTutorials": "模板和教程",
|
||||||
|
"tenMinutes": "10 分钟",
|
||||||
|
"tenSeconds": "10 秒",
|
||||||
|
"testSendUpdate": "测试发送更新",
|
||||||
|
"themeLanguageAndDisplayPreferences": "主题、语言和显示偏好",
|
||||||
|
"thirtyMinutes": "30 分钟",
|
||||||
|
"thirtySeconds": "30 秒",
|
||||||
|
"threeBytesMode2": "3 字节(模式 2)",
|
||||||
|
"trafficStatsMessageHistoryAndDeveloperTools": "流量统计、消息历史和开发者工具",
|
||||||
|
"twoBytesMode1": "2 字节(模式 1)",
|
||||||
|
"twoMinutes": "2 分钟",
|
||||||
|
"undo": "撤销",
|
||||||
|
"updated": "已更新",
|
||||||
|
"useContactFlags": "使用联系人标志",
|
||||||
|
"useSavedCoordinates": "使用已保存的坐标",
|
||||||
|
"view": "查看",
|
||||||
|
"viewNeighbours": "查看邻近节点",
|
||||||
|
"viewPublicStats": "查看公开统计",
|
||||||
|
"setRegionScope": "设置区域范围",
|
||||||
|
"regionScope": "区域范围",
|
||||||
|
"regionScopeNone": "无(全局)",
|
||||||
|
"clearRegionScope": "清除范围",
|
||||||
|
"regionScopeWarning": "只有允许此区域的中继器会转发。",
|
||||||
|
"discoverRegions": "从中继器发现",
|
||||||
|
"discoveringRegions": "正在发现区域...",
|
||||||
|
"enterRegionName": "区域名称(例如 auckland)",
|
||||||
|
"noRegionsFound": "此中继器上未找到区域。",
|
||||||
|
"regionScopeSet": "区域范围已设置为 {name}",
|
||||||
|
"regionScopeCleared": "区域范围已清除",
|
||||||
|
"simpleMode": "简单模式",
|
||||||
|
"simpleModeDescription": "仅与收藏联系人聊天和查看地图",
|
||||||
|
"simpleModeRequiresLockedDestination": "请先启用“锁定目的地”并选择一个频道,再使用简单模式",
|
||||||
|
"noFavouriteContacts": "暂无收藏联系人",
|
||||||
|
"selectDestinationFirst": "请先选择频道、联系人或房间",
|
||||||
|
"recipientNoLongerAvailable": "收件人已不可用",
|
||||||
|
"failedToSendMessage": "发送失败:{error}",
|
||||||
|
"tictactoeDirectOnly": "井字棋仅适用于私聊消息。请先选择联系人。",
|
||||||
|
"deviceKeyUnavailable": "设备密钥不可用",
|
||||||
|
"imageCompressionFailed": "图片压缩失败",
|
||||||
|
"imageFragmentationFailed": "图片分片失败",
|
||||||
|
"failedToAnnounceImage": "图片通告失败",
|
||||||
|
"noRecipientSelected": "未选择收件人",
|
||||||
|
"imageSendFailed": "图片发送失败",
|
||||||
|
"microphonePermissionRequiredForVoice": "语音消息需要麦克风权限",
|
||||||
|
"recordingDiscarded": "已丢弃录音",
|
||||||
|
"publicChannelMediaWarning": "您即将向公共频道发送{mediaType}。不建议这样做,因为 mesh 网络中的所有人都可能收到。除非确有需要,请选择私有或带标签的频道。",
|
||||||
|
"selectPrivateChannelToShareLocation": "请选择私有频道来分享您的位置。",
|
||||||
|
"stopSharingMyLocation": "停止分享我的位置",
|
||||||
|
"shareMyLocation": "分享我的位置",
|
||||||
|
"stopRecording": "停止录音",
|
||||||
|
"recordVoice": "录制语音",
|
||||||
|
"moreActions": "更多操作",
|
||||||
|
"noMessagesInFilter": "此筛选条件下没有消息",
|
||||||
|
"noMatchesInFilter": "此筛选条件下没有匹配项",
|
||||||
|
"syncFailed": "同步失败:{error}",
|
||||||
|
"invalidLink": "无效链接",
|
||||||
|
"cannotOpenLink": "无法打开链接",
|
||||||
|
"failedToOpenLink": "打开链接失败",
|
||||||
|
"failedToResendMessage": "重新发送消息失败",
|
||||||
|
"retryFailed": "重试失败:{error}",
|
||||||
|
"deleteDrawingAndMessage": "删除绘图和消息",
|
||||||
|
"lastEchoRelay": "最后回声:中继",
|
||||||
|
"lastEchoPath": "最后回声:路径",
|
||||||
|
"lastEchoBytesReport": "最后回声:字节报告",
|
||||||
|
"noGpsCoordinatesAvailable": "没有可用的 GPS 坐标",
|
||||||
|
"notASarMarker": "不是搜救标记",
|
||||||
|
"failedToSaveTemplate": "保存模板失败:{error}",
|
||||||
|
"drawingNotFound": "未找到绘图",
|
||||||
|
"unknownDrawingType": "未知绘图类型",
|
||||||
|
"customMapMarker": "自定义地图标记",
|
||||||
|
"customMapPointLabel": "坐标点:{point}",
|
||||||
|
"mapIdLabel": "地图 ID:{id}",
|
||||||
|
"cannotFetchVoice": "无法获取语音",
|
||||||
|
"senderContactUnknown": "发送者联系人未知。请先同步联系人。",
|
||||||
|
"senderRouteUnknown": "发送者路由未知。请先同步联系人/路径。",
|
||||||
|
"messageTooFar": "消息距离过远({hops} 跳,最多 {maxHops} 跳)。",
|
||||||
|
"senderRouteNoPathResponse": "发送者路由未响应路径检测。请同步联系人/路径后重试。",
|
||||||
|
"senderRouteNoRawResponse": "发送者路由在原始传输路径上未响应。",
|
||||||
|
"voiceFetchOverHops": "通过 {hops} 跳获取语音可能需要一些时间。",
|
||||||
|
"voiceReceiveCanceled": "已取消接收语音",
|
||||||
|
"fetchingMissingVoiceFragments": "正在获取缺失的语音分片",
|
||||||
|
"receivingVoice": "正在接收语音",
|
||||||
|
"cannotFetchImage": "无法获取图片",
|
||||||
|
"imageFetchOverHops": "通过 {hops} 跳获取图片可能需要一些时间。",
|
||||||
|
"imageAlreadyBeingReceived": "图片正在接收中",
|
||||||
|
"loadImage": "加载图片",
|
||||||
|
"imageFetchFailedToSendRequest": "图片获取请求发送失败",
|
||||||
|
"imageUnavailable": "图片暂时不可用",
|
||||||
|
"imageFetchTimedOut": "图片获取超时",
|
||||||
|
"imageReceiveCanceled": "已取消接收图片",
|
||||||
|
"closeImagePreview": "关闭图片预览",
|
||||||
|
"recordingReleaseToSend": "正在录音……松开发送",
|
||||||
|
"sendingVoice": "正在发送语音……",
|
||||||
|
"sendLongPressToRecordVoice": "发送(长按录制语音)",
|
||||||
|
"discardRecording": "丢弃录音",
|
||||||
|
"hopCount": "{count, plural, other{{count} 跳}}",
|
||||||
|
"channelsSectionDescription": "用于附近 mesh 通信的广播频道",
|
||||||
|
"roomsSectionDescription": "用于团队持续协调的共享空间",
|
||||||
|
"contactsSectionDescription": "可直接联系的人员和设备",
|
||||||
|
"noRecentChatters": "最近没有人发言",
|
||||||
|
"public": "公共",
|
||||||
|
"failedToSendTicTacToeMove": "井字棋走子发送失败",
|
||||||
|
"youWon": "您赢了",
|
||||||
|
"opponentWon": "对手赢了",
|
||||||
|
"gameDraw": "平局",
|
||||||
|
"yourTurn": "轮到您",
|
||||||
|
"opponentTurn": "轮到对手",
|
||||||
|
"pleaseEnterValidCoordinates": "请输入有效坐标",
|
||||||
|
"locationNotAvailable": "位置不可用",
|
||||||
|
"customCaveMapPoint": "自定义洞穴地图点",
|
||||||
|
"mapPoint": "地图点",
|
||||||
|
"fetchingMissingImageFragments": "正在获取缺失分片……",
|
||||||
|
"loadingImage": "加载中……",
|
||||||
|
"receivingImage": "接收中……",
|
||||||
|
"tapToLoad": "点按加载",
|
||||||
|
"connectDevice": "连接设备",
|
||||||
|
"chooseTransportSubtitle": "选择 Bluetooth、WiFi 或串口传输",
|
||||||
|
"ble": "BLE",
|
||||||
|
"network": "网络",
|
||||||
|
"serial": "串口",
|
||||||
|
"scan": "扫描",
|
||||||
|
"pressScanToSearchForDevices": "点按扫描以搜索附近设备",
|
||||||
|
"failedToConnectToDevice": "连接 {name} 失败",
|
||||||
|
"unknownDevice": "未知设备",
|
||||||
|
"showingCachedResultsTapRefresh": "正在显示缓存结果。点按刷新以重新扫描。",
|
||||||
|
"scanningLocalNetworkOnPort": "正在扫描本地网络中端口 {port} 上的 MeshCore WiFi 设备",
|
||||||
|
"cancelScanAndAddServer": "取消扫描并添加服务器",
|
||||||
|
"addServer": "添加服务器",
|
||||||
|
"scanningProgressIps": "正在扫描……{scanned}/{total} 个 IP",
|
||||||
|
"cancelAndEnterManually": "取消并手动输入",
|
||||||
|
"noRecentOrDiscoveredServers": "暂无最近使用或发现的服务器",
|
||||||
|
"recentlyUsed": "最近使用",
|
||||||
|
"connecting": "正在连接……",
|
||||||
|
"discoveredOnThisNetwork": "在此网络中发现",
|
||||||
|
"serverNoLongerAvailable": "{host}:{port} 上的服务器已不可用。请重新扫描以查找活跃的服务器。",
|
||||||
|
"failedToConnectToHost": "连接 {host}:{port} 失败",
|
||||||
|
"enterValidIpAddress": "请输入有效的 IP 地址",
|
||||||
|
"enterValidTcpPort": "请输入有效的 TCP 端口",
|
||||||
|
"ipAddress": "IP 地址",
|
||||||
|
"tcpPort": "TCP 端口",
|
||||||
|
"customServerPort": "自定义服务器端口",
|
||||||
|
"chat": "聊天",
|
||||||
|
"restoringPreviousLink": "正在恢复先前的连接",
|
||||||
|
"noDeviceConnected": "未连接设备",
|
||||||
|
"sendAdvert": "发送我的联系人信息",
|
||||||
|
"advertMode": "发送我的联系人信息",
|
||||||
|
"advertModeSubtitle": "选择您的联系人信息应传播多远。",
|
||||||
|
"floodAdvertSent": "联系人信息已发送至整个 mesh 网络",
|
||||||
|
"directAdvertSent": "联系人信息已发送至附近设备"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -17,21 +18,28 @@ import 'providers/channels_provider.dart';
|
|||||||
import 'providers/voice_provider.dart';
|
import 'providers/voice_provider.dart';
|
||||||
import 'providers/image_provider.dart' as ip;
|
import 'providers/image_provider.dart' as ip;
|
||||||
import 'providers/app_provider.dart';
|
import 'providers/app_provider.dart';
|
||||||
|
import 'providers/offline_tiles_provider.dart';
|
||||||
import 'providers/sensors_provider.dart';
|
import 'providers/sensors_provider.dart';
|
||||||
import 'services/voice_codec_service.dart';
|
import 'services/voice_codec_service.dart';
|
||||||
import 'services/voice_player_service.dart';
|
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/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 'utils/toast_logger.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());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +51,9 @@ class MeshCoreSarApp extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
||||||
|
static const bool _isScreenshotRun = bool.fromEnvironment(
|
||||||
|
'MESHCORE_SCREENSHOTS',
|
||||||
|
);
|
||||||
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
final GlobalKey<NavigatorState> _navigatorKey = GlobalKey<NavigatorState>();
|
||||||
AppThemeMode _themeMode = AppThemeMode.system;
|
AppThemeMode _themeMode = AppThemeMode.system;
|
||||||
Locale? _locale;
|
Locale? _locale;
|
||||||
@@ -58,6 +69,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();
|
||||||
|
|
||||||
@@ -72,7 +91,9 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
_pendingNotificationPayload = NotificationService().consumeLaunchPayload();
|
||||||
|
|
||||||
// Check if we need to request location permissions
|
// Check if we need to request location permissions
|
||||||
await _checkLocationPermissions();
|
if (!_isScreenshotRun) {
|
||||||
|
await _checkLocationPermissions();
|
||||||
|
}
|
||||||
|
|
||||||
// Check for app updates (Android only) - runs in background
|
// Check for app updates (Android only) - runs in background
|
||||||
// Shows notification if update is available
|
// Shows notification if update is available
|
||||||
@@ -271,6 +292,14 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
),
|
),
|
||||||
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
ChangeNotifierProvider(create: (_) => ChannelsProvider()),
|
||||||
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
ChangeNotifierProvider(create: (_) => SensorsProvider()),
|
||||||
|
ChangeNotifierProvider(create: (_) => OfflineTilesProvider()),
|
||||||
|
ChangeNotifierProvider(
|
||||||
|
create: (_) {
|
||||||
|
final manager = ProfileManager();
|
||||||
|
manager.initialize();
|
||||||
|
return manager;
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
// Voice provider (packet reassembly + playback)
|
// Voice provider (packet reassembly + playback)
|
||||||
ChangeNotifierProvider(
|
ChangeNotifierProvider(
|
||||||
@@ -323,6 +352,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(),
|
||||||
);
|
);
|
||||||
@@ -334,10 +396,11 @@ class _MeshCoreSarAppState extends State<MeshCoreSarApp> {
|
|||||||
final systemBrightness = MediaQuery.platformBrightnessOf(context);
|
final systemBrightness = MediaQuery.platformBrightnessOf(context);
|
||||||
final materialApp = MaterialApp(
|
final materialApp = MaterialApp(
|
||||||
navigatorKey: _navigatorKey,
|
navigatorKey: _navigatorKey,
|
||||||
|
scaffoldMessengerKey: rootScaffoldMessengerKey,
|
||||||
key: ValueKey<String?>(
|
key: ValueKey<String?>(
|
||||||
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
|
'${_locale?.languageCode ?? 'system'}_${_themeMode.name}',
|
||||||
),
|
),
|
||||||
title: 'MeshCore SAR',
|
onGenerateTitle: (context) => AppLocalizations.of(context)?.appTitle ?? 'MeshCore SAR',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
theme: AppTheme.getTheme(_themeMode, systemBrightness),
|
theme: AppTheme.getTheme(_themeMode, systemBrightness),
|
||||||
locale: _locale,
|
locale: _locale,
|
||||||
|
|||||||
@@ -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,18 @@ 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);
|
||||||
|
|
||||||
|
/// MeshCore group packets carry the first byte of SHA256(channel secret).
|
||||||
|
int get hashByte {
|
||||||
|
final digest = sha256.convert(secret);
|
||||||
|
return digest.bytes.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
String get hashHex =>
|
||||||
|
hashByte.toRadixString(16).padLeft(2, '0').toUpperCase();
|
||||||
|
|
||||||
/// 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 +167,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,
|
||||||
|
|||||||
518
lib/models/config_profile.dart
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
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? 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.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 &&
|
||||||
|
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,
|
||||||
|
'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(),
|
||||||
|
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;
|
||||||
@@ -179,6 +196,54 @@ class ContactRouteCodec {
|
|||||||
final hopCount = raw & 0x3F;
|
final hopCount = raw & 0x3F;
|
||||||
return hopCount * hashSize <= maxPathBytes;
|
return hopCount * hashSize <= maxPathBytes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an encoded MeshCore route descriptor + path into the legacy raw
|
||||||
|
/// path format expected by the `CMD_SEND_RAW_DATA` (0x19) handler in
|
||||||
|
/// companion firmware v1.15.
|
||||||
|
///
|
||||||
|
/// The raw-data handler treats the path-length byte as a *literal* hop count
|
||||||
|
/// (one byte per hop) rather than as an encoded descriptor, so a 1-hop route
|
||||||
|
/// using 2-byte hashes (descriptor `0x41`) would otherwise be misread as
|
||||||
|
/// "65 path bytes" and rejected with `ERROR: Unsupported command`. We
|
||||||
|
/// down-sample every hop to its first hash byte and emit a plain hop count.
|
||||||
|
///
|
||||||
|
/// Routes that are already legacy (hash size 1, descriptor `< 0x40`) pass
|
||||||
|
/// through unchanged. Unknown/flood descriptors collapse to a zero-hop
|
||||||
|
/// direct path (raw transport does not support flood routing).
|
||||||
|
static LegacyRawPath toLegacyRawPath(int descriptor, Uint8List path) {
|
||||||
|
final raw = toUnsignedDescriptor(descriptor);
|
||||||
|
if (raw == _unknownDescriptor) {
|
||||||
|
return const LegacyRawPath(length: 0, path: <int>[]);
|
||||||
|
}
|
||||||
|
if (raw < 0x40) {
|
||||||
|
// Hash size 1: the encoded path is already one byte per hop.
|
||||||
|
final hopCount = raw;
|
||||||
|
final available = math.min(hopCount, path.length);
|
||||||
|
return LegacyRawPath(
|
||||||
|
length: hopCount,
|
||||||
|
path: Uint8List.fromList(path.sublist(0, available)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final hopCount = raw & 0x3F;
|
||||||
|
final hashSize = ((raw >> 6) & 0x03) + 1;
|
||||||
|
final legacy = Uint8List(hopCount);
|
||||||
|
for (var hop = 0; hop < hopCount; hop++) {
|
||||||
|
final srcIndex = hop * hashSize;
|
||||||
|
if (srcIndex < path.length) {
|
||||||
|
legacy[hop] = path[srcIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return LegacyRawPath(length: hopCount, path: legacy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Legacy raw-transport path: a literal hop count plus one byte per hop, as
|
||||||
|
/// expected by the firmware `CMD_SEND_RAW_DATA` handler.
|
||||||
|
class LegacyRawPath {
|
||||||
|
const LegacyRawPath({required this.length, required this.path});
|
||||||
|
|
||||||
|
final int length;
|
||||||
|
final List<int> path;
|
||||||
}
|
}
|
||||||
|
|
||||||
extension ContactLocalization on Contact {
|
extension ContactLocalization on Contact {
|
||||||
|
|||||||
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';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,6 +71,7 @@ class DeviceInfo {
|
|||||||
final bool? autoAddRoomServers;
|
final bool? autoAddRoomServers;
|
||||||
final bool? autoAddSensors;
|
final bool? autoAddSensors;
|
||||||
final bool? autoAddOverwriteOldest;
|
final bool? autoAddOverwriteOldest;
|
||||||
|
final int? autoAddMaxHops;
|
||||||
final int? radioFreq;
|
final int? radioFreq;
|
||||||
final int? radioBw;
|
final int? radioBw;
|
||||||
final int? radioSf;
|
final int? radioSf;
|
||||||
@@ -95,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;
|
||||||
@@ -123,6 +125,7 @@ class DeviceInfo {
|
|||||||
this.autoAddRoomServers,
|
this.autoAddRoomServers,
|
||||||
this.autoAddSensors,
|
this.autoAddSensors,
|
||||||
this.autoAddOverwriteOldest,
|
this.autoAddOverwriteOldest,
|
||||||
|
this.autoAddMaxHops,
|
||||||
this.radioFreq,
|
this.radioFreq,
|
||||||
this.radioBw,
|
this.radioBw,
|
||||||
this.radioSf,
|
this.radioSf,
|
||||||
@@ -139,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,
|
||||||
@@ -254,6 +258,7 @@ class DeviceInfo {
|
|||||||
bool? autoAddRoomServers,
|
bool? autoAddRoomServers,
|
||||||
bool? autoAddSensors,
|
bool? autoAddSensors,
|
||||||
bool? autoAddOverwriteOldest,
|
bool? autoAddOverwriteOldest,
|
||||||
|
int? autoAddMaxHops,
|
||||||
int? radioFreq,
|
int? radioFreq,
|
||||||
int? radioBw,
|
int? radioBw,
|
||||||
int? radioSf,
|
int? radioSf,
|
||||||
@@ -270,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,
|
||||||
@@ -299,6 +305,7 @@ class DeviceInfo {
|
|||||||
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
autoAddSensors: autoAddSensors ?? this.autoAddSensors,
|
||||||
autoAddOverwriteOldest:
|
autoAddOverwriteOldest:
|
||||||
autoAddOverwriteOldest ?? this.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,
|
||||||
@@ -315,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
@@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ enum MapLayerType {
|
|||||||
// Kept for stored preference compatibility after MBTiles removal.
|
// Kept for stored preference compatibility after MBTiles removal.
|
||||||
vectorMbtiles,
|
vectorMbtiles,
|
||||||
wmsBase,
|
wmsBase,
|
||||||
|
// Appended last: stored layer preference is the enum index.
|
||||||
|
mapyOutdoor,
|
||||||
}
|
}
|
||||||
|
|
||||||
class MapLayer {
|
class MapLayer {
|
||||||
@@ -21,6 +23,9 @@ class MapLayer {
|
|||||||
final String attribution;
|
final String attribution;
|
||||||
final double maxZoom;
|
final double maxZoom;
|
||||||
|
|
||||||
|
/// Extra HTTP headers required by the tile server (e.g. Referer).
|
||||||
|
final Map<String, String>? headers;
|
||||||
|
|
||||||
// WMS specific properties
|
// WMS specific properties
|
||||||
final bool isWms;
|
final bool isWms;
|
||||||
final String? wmsBaseUrl;
|
final String? wmsBaseUrl;
|
||||||
@@ -36,6 +41,7 @@ class MapLayer {
|
|||||||
required this.urlTemplate,
|
required this.urlTemplate,
|
||||||
required this.attribution,
|
required this.attribution,
|
||||||
required this.maxZoom,
|
required this.maxZoom,
|
||||||
|
this.headers,
|
||||||
this.isWms = false,
|
this.isWms = false,
|
||||||
this.wmsBaseUrl,
|
this.wmsBaseUrl,
|
||||||
this.wmsLayers,
|
this.wmsLayers,
|
||||||
@@ -64,6 +70,9 @@ class MapLayer {
|
|||||||
case MapLayerType.vectorMbtiles:
|
case MapLayerType.vectorMbtiles:
|
||||||
// Legacy value kept only for preference migration compatibility.
|
// Legacy value kept only for preference migration compatibility.
|
||||||
return name;
|
return name;
|
||||||
|
case MapLayerType.mapyOutdoor:
|
||||||
|
// Brand name, not translated.
|
||||||
|
return name;
|
||||||
case MapLayerType.wmsBase:
|
case MapLayerType.wmsBase:
|
||||||
// For WMS layers, use the name (will be localized separately)
|
// For WMS layers, use the name (will be localized separately)
|
||||||
return name;
|
return name;
|
||||||
@@ -86,6 +95,17 @@ class MapLayer {
|
|||||||
maxZoom: 17.49, // OpenTopoMap maximum (just below level 18)
|
maxZoom: 17.49, // OpenTopoMap maximum (just below level 18)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// Mapy.cz outdoor map: OSM data with LIDAR-based shaded relief.
|
||||||
|
/// The tile server rejects requests without a mapy.com Referer.
|
||||||
|
static const mapyOutdoor = MapLayer(
|
||||||
|
type: MapLayerType.mapyOutdoor,
|
||||||
|
name: 'Mapy.cz Outdoor (OSM + LIDAR)',
|
||||||
|
urlTemplate: 'https://mapserver.mapy.cz/turist-en/retina/{z}-{x}-{y}',
|
||||||
|
attribution: '© Seznam.cz, a.s., © OpenStreetMap contributors',
|
||||||
|
maxZoom: 19,
|
||||||
|
headers: {'Referer': 'https://mapy.com/'},
|
||||||
|
);
|
||||||
|
|
||||||
static const esriWorldImagery = MapLayer(
|
static const esriWorldImagery = MapLayer(
|
||||||
type: MapLayerType.esriWorldImagery,
|
type: MapLayerType.esriWorldImagery,
|
||||||
name: 'ESRI Satellite',
|
name: 'ESRI Satellite',
|
||||||
@@ -160,6 +180,7 @@ class MapLayer {
|
|||||||
static const List<MapLayer> allLayers = [
|
static const List<MapLayer> allLayers = [
|
||||||
openStreetMap,
|
openStreetMap,
|
||||||
openTopoMap,
|
openTopoMap,
|
||||||
|
mapyOutdoor,
|
||||||
esriWorldImagery,
|
esriWorldImagery,
|
||||||
googleHybrid,
|
googleHybrid,
|
||||||
googleRoadmap,
|
googleRoadmap,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,14 @@ class MessageContactLocation {
|
|||||||
|
|
||||||
String get technicalSourceLabel {
|
String get technicalSourceLabel {
|
||||||
switch (source) {
|
switch (source) {
|
||||||
|
case 'shared':
|
||||||
|
return 'shared location';
|
||||||
|
case 'gps':
|
||||||
|
return 'shared location';
|
||||||
case 'telemetry':
|
case 'telemetry':
|
||||||
return 'telemetry';
|
return 'live telemetry';
|
||||||
case 'advert':
|
case 'advert':
|
||||||
return 'advert';
|
return 'shared advert';
|
||||||
default:
|
default:
|
||||||
return source;
|
return source;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,197 +0,0 @@
|
|||||||
enum PathRecordSource { learned, observed }
|
|
||||||
|
|
||||||
class PathRecord {
|
|
||||||
final List<int> pathBytes;
|
|
||||||
final int hopCount;
|
|
||||||
final int hashSize;
|
|
||||||
final PathRecordSource source;
|
|
||||||
final int successCount;
|
|
||||||
final int failureCount;
|
|
||||||
final int lastRoundTripTimeMs;
|
|
||||||
final DateTime lastUsedAt;
|
|
||||||
|
|
||||||
const PathRecord({
|
|
||||||
required this.pathBytes,
|
|
||||||
required this.hopCount,
|
|
||||||
required this.hashSize,
|
|
||||||
required this.source,
|
|
||||||
required this.successCount,
|
|
||||||
required this.failureCount,
|
|
||||||
required this.lastRoundTripTimeMs,
|
|
||||||
required this.lastUsedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
String get signature =>
|
|
||||||
pathBytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join();
|
|
||||||
|
|
||||||
double get successRate =>
|
|
||||||
(successCount + 1) / (successCount + failureCount + 2);
|
|
||||||
|
|
||||||
PathRecord copyWith({
|
|
||||||
List<int>? pathBytes,
|
|
||||||
int? hopCount,
|
|
||||||
int? hashSize,
|
|
||||||
PathRecordSource? source,
|
|
||||||
int? successCount,
|
|
||||||
int? failureCount,
|
|
||||||
int? lastRoundTripTimeMs,
|
|
||||||
DateTime? lastUsedAt,
|
|
||||||
}) {
|
|
||||||
return PathRecord(
|
|
||||||
pathBytes: pathBytes ?? this.pathBytes,
|
|
||||||
hopCount: hopCount ?? this.hopCount,
|
|
||||||
hashSize: hashSize ?? this.hashSize,
|
|
||||||
source: source ?? this.source,
|
|
||||||
successCount: successCount ?? this.successCount,
|
|
||||||
failureCount: failureCount ?? this.failureCount,
|
|
||||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
|
||||||
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'path_bytes': pathBytes,
|
|
||||||
'hop_count': hopCount,
|
|
||||||
'hash_size': hashSize,
|
|
||||||
'source': source.name,
|
|
||||||
'success_count': successCount,
|
|
||||||
'failure_count': failureCount,
|
|
||||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
|
||||||
'last_used_at': lastUsedAt.toIso8601String(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory PathRecord.fromJson(Map<String, dynamic> json) {
|
|
||||||
return PathRecord(
|
|
||||||
pathBytes: (json['path_bytes'] as List<dynamic>? ?? const <dynamic>[])
|
|
||||||
.map((value) => value as int)
|
|
||||||
.toList(),
|
|
||||||
hopCount: json['hop_count'] as int? ?? 0,
|
|
||||||
hashSize: json['hash_size'] as int? ?? 1,
|
|
||||||
source: PathRecordSource.values.firstWhere(
|
|
||||||
(value) => value.name == (json['source'] as String? ?? 'learned'),
|
|
||||||
orElse: () => PathRecordSource.learned,
|
|
||||||
),
|
|
||||||
successCount: json['success_count'] as int? ?? 0,
|
|
||||||
failureCount: json['failure_count'] as int? ?? 0,
|
|
||||||
lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0,
|
|
||||||
lastUsedAt:
|
|
||||||
DateTime.tryParse(json['last_used_at'] as String? ?? '') ??
|
|
||||||
DateTime.fromMillisecondsSinceEpoch(0),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class FloodPathStats {
|
|
||||||
final int successCount;
|
|
||||||
final int failureCount;
|
|
||||||
final int lastRoundTripTimeMs;
|
|
||||||
final DateTime? lastUsedAt;
|
|
||||||
|
|
||||||
const FloodPathStats({
|
|
||||||
required this.successCount,
|
|
||||||
required this.failureCount,
|
|
||||||
required this.lastRoundTripTimeMs,
|
|
||||||
required this.lastUsedAt,
|
|
||||||
});
|
|
||||||
|
|
||||||
const FloodPathStats.empty()
|
|
||||||
: successCount = 0,
|
|
||||||
failureCount = 0,
|
|
||||||
lastRoundTripTimeMs = 0,
|
|
||||||
lastUsedAt = null;
|
|
||||||
|
|
||||||
FloodPathStats copyWith({
|
|
||||||
int? successCount,
|
|
||||||
int? failureCount,
|
|
||||||
int? lastRoundTripTimeMs,
|
|
||||||
DateTime? lastUsedAt,
|
|
||||||
}) {
|
|
||||||
return FloodPathStats(
|
|
||||||
successCount: successCount ?? this.successCount,
|
|
||||||
failureCount: failureCount ?? this.failureCount,
|
|
||||||
lastRoundTripTimeMs: lastRoundTripTimeMs ?? this.lastRoundTripTimeMs,
|
|
||||||
lastUsedAt: lastUsedAt ?? this.lastUsedAt,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'success_count': successCount,
|
|
||||||
'failure_count': failureCount,
|
|
||||||
'last_round_trip_time_ms': lastRoundTripTimeMs,
|
|
||||||
'last_used_at': lastUsedAt?.toIso8601String(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
factory FloodPathStats.fromJson(Map<String, dynamic> json) {
|
|
||||||
return FloodPathStats(
|
|
||||||
successCount: json['success_count'] as int? ?? 0,
|
|
||||||
failureCount: json['failure_count'] as int? ?? 0,
|
|
||||||
lastRoundTripTimeMs: json['last_round_trip_time_ms'] as int? ?? 0,
|
|
||||||
lastUsedAt: DateTime.tryParse(json['last_used_at'] as String? ?? ''),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContactPathHistory {
|
|
||||||
final String contactPublicKeyHex;
|
|
||||||
final List<PathRecord> directPaths;
|
|
||||||
final FloodPathStats floodStats;
|
|
||||||
final int rotationIndex;
|
|
||||||
|
|
||||||
const ContactPathHistory({
|
|
||||||
required this.contactPublicKeyHex,
|
|
||||||
required this.directPaths,
|
|
||||||
required this.floodStats,
|
|
||||||
required this.rotationIndex,
|
|
||||||
});
|
|
||||||
|
|
||||||
const ContactPathHistory.empty(this.contactPublicKeyHex)
|
|
||||||
: directPaths = const <PathRecord>[],
|
|
||||||
floodStats = const FloodPathStats.empty(),
|
|
||||||
rotationIndex = 0;
|
|
||||||
|
|
||||||
ContactPathHistory copyWith({
|
|
||||||
List<PathRecord>? directPaths,
|
|
||||||
FloodPathStats? floodStats,
|
|
||||||
int? rotationIndex,
|
|
||||||
}) {
|
|
||||||
return ContactPathHistory(
|
|
||||||
contactPublicKeyHex: contactPublicKeyHex,
|
|
||||||
directPaths: directPaths ?? this.directPaths,
|
|
||||||
floodStats: floodStats ?? this.floodStats,
|
|
||||||
rotationIndex: rotationIndex ?? this.rotationIndex,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
|
||||||
return {
|
|
||||||
'direct_paths': directPaths.map((record) => record.toJson()).toList(),
|
|
||||||
'flood_stats': floodStats.toJson(),
|
|
||||||
'rotation_index': rotationIndex,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
List<PathRecord> get observedPaths => directPaths
|
|
||||||
.where((record) => record.source == PathRecordSource.observed)
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
factory ContactPathHistory.fromJson(
|
|
||||||
String contactPublicKeyHex,
|
|
||||||
Map<String, dynamic> json,
|
|
||||||
) {
|
|
||||||
return ContactPathHistory(
|
|
||||||
contactPublicKeyHex: contactPublicKeyHex,
|
|
||||||
directPaths: (json['direct_paths'] as List<dynamic>? ?? const <dynamic>[])
|
|
||||||
.whereType<Map<String, dynamic>>()
|
|
||||||
.map(PathRecord.fromJson)
|
|
||||||
.toList(),
|
|
||||||
floodStats: json['flood_stats'] is Map<String, dynamic>
|
|
||||||
? FloodPathStats.fromJson(json['flood_stats'] as Map<String, dynamic>)
|
|
||||||
: const FloodPathStats.empty(),
|
|
||||||
rotationIndex: json['rotation_index'] as int? ?? 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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})';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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];
|
||||||
@@ -28,6 +29,16 @@ class ChannelsProvider with ChangeNotifier {
|
|||||||
return index == 0 ? 'Public' : 'Channel $index';
|
return index == 0 ? 'Public' : 'Channel $index';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Channel> getChannelsByHashByte(int hashByte) {
|
||||||
|
return _channels.values.where((channel) => channel.hashByte == hashByte).toList()
|
||||||
|
..sort((a, b) => a.index.compareTo(b.index));
|
||||||
|
}
|
||||||
|
|
||||||
|
String? getUniqueChannelDisplayNameByHashByte(int hashByte) {
|
||||||
|
final matches = getChannelsByHashByte(hashByte);
|
||||||
|
return matches.length == 1 ? matches.single.displayName : null;
|
||||||
|
}
|
||||||
|
|
||||||
/// Add or update a channel
|
/// Add or update a channel
|
||||||
void addOrUpdateChannel({
|
void addOrUpdateChannel({
|
||||||
required int index,
|
required int index,
|
||||||
@@ -54,12 +65,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 +107,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;
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,9 @@ import '../models/contact_group.dart';
|
|||||||
import '../models/message_contact_location.dart';
|
import '../models/message_contact_location.dart';
|
||||||
import '../services/cayenne_lpp_parser.dart';
|
import '../services/cayenne_lpp_parser.dart';
|
||||||
import '../services/contact_storage_service.dart';
|
import '../services/contact_storage_service.dart';
|
||||||
|
import '../services/profiles_feature_service.dart';
|
||||||
import '../utils/fast_gps_packet.dart';
|
import '../utils/fast_gps_packet.dart';
|
||||||
|
import '../utils/rssi_location_estimator.dart';
|
||||||
import '../utils/key_comparison.dart';
|
import '../utils/key_comparison.dart';
|
||||||
|
|
||||||
class PendingAdvert {
|
class PendingAdvert {
|
||||||
@@ -125,85 +127,99 @@ class _RetainedRoute {
|
|||||||
class ContactsProvider with ChangeNotifier {
|
class ContactsProvider with ChangeNotifier {
|
||||||
static const double _firstHopFallbackOffsetMeters = 100.0;
|
static const double _firstHopFallbackOffsetMeters = 100.0;
|
||||||
static const String autoGroupIdPrefix = 'auto_group_';
|
static const String autoGroupIdPrefix = 'auto_group_';
|
||||||
|
static const String _rawTelemetryHexKey = '__raw_lpp_hex';
|
||||||
final Map<String, Contact> _contacts = {};
|
final Map<String, Contact> _contacts = {};
|
||||||
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
final List<SavedContactGroup> _savedContactGroups = <SavedContactGroup>[];
|
||||||
final Map<String, PendingAdvert> _pendingAdverts = {};
|
final Map<String, PendingAdvert> _pendingAdverts = {};
|
||||||
|
final Map<String, LatLng> _estimatedLocations = {};
|
||||||
|
final Map<String, List<RssiObservation>> _rssiObservations = {};
|
||||||
|
final Map<String, Contact> _retainedContactsForSync = {};
|
||||||
final ContactStorageService _storageService = ContactStorageService();
|
final ContactStorageService _storageService = ContactStorageService();
|
||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _isPersisting = false;
|
bool _isPersisting = false;
|
||||||
bool _persistRequested = false;
|
bool _persistRequested = false;
|
||||||
bool _isPersistingPendingAdverts = false;
|
bool _isPersistingPendingAdverts = false;
|
||||||
bool _persistPendingAdvertsRequested = false;
|
bool _persistPendingAdvertsRequested = false;
|
||||||
|
String? _storageNamespace;
|
||||||
|
String? _selfPublicKeyHex;
|
||||||
|
ContactTelemetry? _selfTelemetry;
|
||||||
|
|
||||||
// Add default public channel on initialization
|
// Add default public channel on initialization
|
||||||
ContactsProvider() {
|
ContactsProvider()
|
||||||
|
: _storageNamespace = ProfileStorageScope.effectiveNamespace {
|
||||||
_ensurePublicChannelExists();
|
_ensurePublicChannelExists();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get isInitialized => _isInitialized;
|
bool get isInitialized => _isInitialized;
|
||||||
|
String? get storageNamespace => _storageNamespace;
|
||||||
|
|
||||||
/// Initialize and load persisted contacts at app startup
|
/// Initialize and load persisted contacts at app startup
|
||||||
/// This loads contacts without filtering, allowing offline viewing
|
/// This loads contacts without filtering, allowing offline viewing
|
||||||
Future<void> initializeEarly() async {
|
Future<void> initializeEarly() async {
|
||||||
if (_isInitialized) return;
|
if (_isInitialized) return;
|
||||||
|
|
||||||
|
await _loadFromStorage();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> reloadFromStorage({
|
||||||
|
String? namespace,
|
||||||
|
Uint8List? devicePublicKey,
|
||||||
|
}) async {
|
||||||
|
_storageNamespace = namespace;
|
||||||
|
_setSelfDevicePublicKey(devicePublicKey);
|
||||||
|
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> persistNow() async {
|
||||||
|
await _storageService.saveContacts(
|
||||||
|
_contactsForStorage(),
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
await _storageService.saveContactGroups(
|
||||||
|
_savedContactGroups,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
await _storageService.savePendingAdverts(
|
||||||
|
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> cloneCurrentStorageTo(String? namespace) async {
|
||||||
|
await _storageService.saveContacts(
|
||||||
|
_contactsForStorage(),
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
|
await _storageService.saveContactGroups(
|
||||||
|
_savedContactGroups,
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
|
await _storageService.savePendingAdverts(
|
||||||
|
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||||
|
namespace: namespace,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadFromStorage({
|
||||||
|
bool force = false,
|
||||||
|
Uint8List? devicePublicKey,
|
||||||
|
}) async {
|
||||||
|
if (_isInitialized && !force) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
_resetInMemoryState();
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'📦 [ContactsProvider] Early loading persisted contacts (no filtering)...',
|
'📦 [ContactsProvider] Early loading persisted contacts (no filtering)...',
|
||||||
);
|
);
|
||||||
final storedContacts = await _storageService.loadContacts();
|
|
||||||
final storedGroups = await _storageService.loadContactGroups();
|
|
||||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
|
||||||
|
|
||||||
// Add stored contacts (excluding any with all-zeros public key)
|
|
||||||
const publicChannelKey =
|
|
||||||
'0000000000000000000000000000000000000000000000000000000000000000';
|
|
||||||
for (final contact in storedContacts) {
|
|
||||||
// Skip any contacts with all-zeros public key (shouldn't happen, but safety check)
|
|
||||||
if (contact.publicKeyHex == publicChannelKey) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
_contacts[contact.publicKeyHex] = contact;
|
|
||||||
}
|
|
||||||
|
|
||||||
_isInitialized = true;
|
|
||||||
_savedContactGroups
|
|
||||||
..clear()
|
|
||||||
..addAll(storedGroups);
|
|
||||||
_restorePendingAdverts(storedPendingAdverts);
|
|
||||||
debugPrint(
|
|
||||||
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
|
||||||
);
|
|
||||||
|
|
||||||
// Ensure public channel exists after loading
|
|
||||||
_ensurePublicChannelExists();
|
|
||||||
|
|
||||||
notifyListeners();
|
|
||||||
} catch (e) {
|
|
||||||
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
|
|
||||||
_isInitialized = true; // Mark as initialized even on error
|
|
||||||
_ensurePublicChannelExists();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initialize and load persisted contacts
|
|
||||||
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
|
||||||
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
|
||||||
if (_isInitialized) {
|
|
||||||
// If already initialized (from early load), just filter out self-contact
|
|
||||||
if (devicePublicKey != null) {
|
|
||||||
_removeSelfContact(devicePublicKey);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
debugPrint('📦 [ContactsProvider] Loading persisted contacts...');
|
|
||||||
final storedContacts = await _storageService.loadContacts(
|
final storedContacts = await _storageService.loadContacts(
|
||||||
excludePublicKey: devicePublicKey,
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
final storedGroups = await _storageService.loadContactGroups(
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
final storedPendingAdverts = await _storageService.loadPendingAdverts(
|
||||||
|
namespace: _storageNamespace,
|
||||||
);
|
);
|
||||||
final storedGroups = await _storageService.loadContactGroups();
|
|
||||||
final storedPendingAdverts = await _storageService.loadPendingAdverts();
|
|
||||||
|
|
||||||
// Add stored contacts (excluding any with all-zeros public key)
|
// Add stored contacts (excluding any with all-zeros public key)
|
||||||
const publicChannelKey =
|
const publicChannelKey =
|
||||||
@@ -225,7 +241,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
devicePublicKey: devicePublicKey,
|
devicePublicKey: devicePublicKey,
|
||||||
);
|
);
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'✅ [ContactsProvider] Loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
'✅ [ContactsProvider] Early loaded ${storedContacts.length} persisted contacts and ${storedGroups.length} groups',
|
||||||
);
|
);
|
||||||
|
|
||||||
// Ensure public channel exists after loading
|
// Ensure public channel exists after loading
|
||||||
@@ -233,12 +249,99 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ContactsProvider] Error initializing: $e');
|
debugPrint('❌ [ContactsProvider] Error in early initialization: $e');
|
||||||
_isInitialized = true; // Mark as initialized even on error
|
_isInitialized = true; // Mark as initialized even on error
|
||||||
_ensurePublicChannelExists();
|
_ensurePublicChannelExists();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Initialize and load persisted contacts
|
||||||
|
/// [devicePublicKey] - device's own public key to exclude from loaded contacts
|
||||||
|
Future<void> initialize({Uint8List? devicePublicKey}) async {
|
||||||
|
_setSelfDevicePublicKey(devicePublicKey);
|
||||||
|
if (_isInitialized) {
|
||||||
|
// If already initialized (from early load), just filter out self-contact
|
||||||
|
if (devicePublicKey != null) {
|
||||||
|
_removeSelfContact(devicePublicKey);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _loadFromStorage(force: true, devicePublicKey: devicePublicKey);
|
||||||
|
if (devicePublicKey != null) {
|
||||||
|
_removeSelfContact(devicePublicKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear runtime contact state before a live device contact sync begins.
|
||||||
|
///
|
||||||
|
/// This intentionally does not touch persisted storage. It snapshots the
|
||||||
|
/// current in-memory contacts so sync updates can still merge against the
|
||||||
|
/// latest local state, but keeps the visible list intact so reconnects do
|
||||||
|
/// not blank the UI while the device is resyncing.
|
||||||
|
Future<void> prepareForDeviceContactSync({Uint8List? devicePublicKey}) async {
|
||||||
|
_setSelfDevicePublicKey(devicePublicKey);
|
||||||
|
if (!_isInitialized) {
|
||||||
|
final storedGroups = await _storageService.loadContactGroups(
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
|
_savedContactGroups
|
||||||
|
..clear()
|
||||||
|
..addAll(storedGroups);
|
||||||
|
_isInitialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'🧹 [ContactsProvider] Preparing retained contact state for device sync',
|
||||||
|
);
|
||||||
|
_retainedContactsForSync
|
||||||
|
..clear()
|
||||||
|
..addEntries(
|
||||||
|
_contactsForStorage().map(
|
||||||
|
(contact) => MapEntry(contact.publicKeyHex, contact),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finish a device contact sync started with [prepareForDeviceContactSync].
|
||||||
|
///
|
||||||
|
/// The device contact list is authoritative: when [deviceListComplete] is
|
||||||
|
/// true, any contact still left in the retained snapshot (i.e. not sent by
|
||||||
|
/// the device during this sync) is deleted locally. Channel pseudo-contacts
|
||||||
|
/// are excluded — they mirror device channel slots and are reconciled by the
|
||||||
|
/// channel sync instead. When the sync was partial (timeout/disconnect),
|
||||||
|
/// nothing is deleted so a flaky link can't wipe valid contacts.
|
||||||
|
void finalizeDeviceContactSync({required bool deviceListComplete}) {
|
||||||
|
if (!deviceListComplete) {
|
||||||
|
if (_retainedContactsForSync.isNotEmpty) {
|
||||||
|
debugPrint(
|
||||||
|
'ℹ️ [ContactsProvider] Partial contact sync - keeping '
|
||||||
|
'${_retainedContactsForSync.length} unconfirmed contact(s)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_retainedContactsForSync.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final staleKeys = _retainedContactsForSync.keys
|
||||||
|
.where((key) => _contacts[key]?.type != ContactType.channel)
|
||||||
|
.where(_contacts.containsKey)
|
||||||
|
.toList();
|
||||||
|
for (final key in staleKeys) {
|
||||||
|
final removed = _contacts.remove(key);
|
||||||
|
debugPrint(
|
||||||
|
'🗑️ [ContactsProvider] Removing contact no longer on device: '
|
||||||
|
'${removed?.advName} (${key.substring(0, 8)}...)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
_retainedContactsForSync.clear();
|
||||||
|
|
||||||
|
if (staleKeys.isNotEmpty) {
|
||||||
|
_persistContacts();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
/// Remove self-contact from loaded contacts (called after BLE connection established)
|
||||||
void _removeSelfContact(Uint8List devicePublicKey) {
|
void _removeSelfContact(Uint8List devicePublicKey) {
|
||||||
final selfKeyHex = devicePublicKey
|
final selfKeyHex = devicePublicKey
|
||||||
@@ -287,7 +390,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
try {
|
try {
|
||||||
while (_persistRequested) {
|
while (_persistRequested) {
|
||||||
_persistRequested = false;
|
_persistRequested = false;
|
||||||
await _storageService.saveContacts(_contactsForStorage());
|
await _storageService.saveContacts(
|
||||||
|
_contactsForStorage(),
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
|
debugPrint('❌ [ContactsProvider] Error persisting contacts: $e');
|
||||||
@@ -305,6 +411,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
_persistPendingAdvertsRequested = false;
|
_persistPendingAdvertsRequested = false;
|
||||||
await _storageService.savePendingAdverts(
|
await _storageService.savePendingAdverts(
|
||||||
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
_pendingAdverts.values.map(_pendingAdvertToJson).toList(),
|
||||||
|
namespace: _storageNamespace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -315,6 +422,13 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
List<Contact> get contacts => _contacts.values.toList();
|
List<Contact> get contacts => _contacts.values.toList();
|
||||||
|
ContactTelemetry? get selfTelemetry => _selfTelemetry;
|
||||||
|
List<Contact> get favouriteContacts =>
|
||||||
|
_contacts.values.where((c) => c.isFavourite).toList();
|
||||||
|
List<Contact> get favouriteChatContacts =>
|
||||||
|
chatContacts.where((c) => c.isFavourite).toList();
|
||||||
|
List<Contact> get favouriteContactsWithLocation =>
|
||||||
|
favouriteContacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||||
List<SavedContactGroup> get savedContactGroups =>
|
List<SavedContactGroup> get savedContactGroups =>
|
||||||
List<SavedContactGroup>.from(_savedContactGroups)
|
List<SavedContactGroup>.from(_savedContactGroups)
|
||||||
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
..sort((a, b) => b.createdAt.compareTo(a.createdAt));
|
||||||
@@ -436,7 +550,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
Future<void> _persistSavedGroups() async {
|
Future<void> _persistSavedGroups() async {
|
||||||
try {
|
try {
|
||||||
await _storageService.saveContactGroups(_savedContactGroups);
|
await _storageService.saveContactGroups(
|
||||||
|
_savedContactGroups,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint('❌ [ContactsProvider] Error persisting contact groups: $e');
|
debugPrint('❌ [ContactsProvider] Error persisting contact groups: $e');
|
||||||
}
|
}
|
||||||
@@ -450,6 +567,9 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
List<Contact> get repeaters =>
|
List<Contact> get repeaters =>
|
||||||
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
contacts.where((c) => c.isRepeater).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
|
List<Contact> get sensorContacts =>
|
||||||
|
contacts.where((c) => c.isSensor).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
List<Contact> get rooms =>
|
List<Contact> get rooms =>
|
||||||
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
contacts.where((c) => c.isRoom).toList()..sort(_sortByLastSeen);
|
||||||
|
|
||||||
@@ -466,13 +586,73 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
..sort(_sortByLastSeen);
|
..sort(_sortByLastSeen);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// All estimated locations (for passing to map marker service).
|
||||||
|
Map<String, LatLng> get estimatedLocations =>
|
||||||
|
Map.unmodifiable(_estimatedLocations);
|
||||||
|
|
||||||
|
/// Get estimated location for a contact (RSSI-based, near last-hop repeater).
|
||||||
|
LatLng? estimatedLocationFor(String publicKeyHex) =>
|
||||||
|
_estimatedLocations[publicKeyHex];
|
||||||
|
|
||||||
|
/// Set an estimated location for a contact that has no GPS.
|
||||||
|
void setEstimatedLocation(String publicKeyHex, LatLng location) {
|
||||||
|
_estimatedLocations[publicKeyHex] = location;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Record an RSSI observation and re-trilaterate the contact's position.
|
||||||
|
///
|
||||||
|
/// Keeps up to 5 observations per unique repeater (latest wins).
|
||||||
|
/// With multiple repeaters, trilateration produces a better estimate.
|
||||||
|
void addRssiObservation({
|
||||||
|
required String contactPublicKeyHex,
|
||||||
|
required List<int> contactPublicKey,
|
||||||
|
required RssiObservation observation,
|
||||||
|
}) {
|
||||||
|
final observations = _rssiObservations.putIfAbsent(
|
||||||
|
contactPublicKeyHex,
|
||||||
|
() => [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace existing observation from the same repeater, or add new
|
||||||
|
final repeaterKey =
|
||||||
|
'${observation.repeaterLocation.latitude},${observation.repeaterLocation.longitude}';
|
||||||
|
observations.removeWhere(
|
||||||
|
(o) =>
|
||||||
|
'${o.repeaterLocation.latitude},${o.repeaterLocation.longitude}' ==
|
||||||
|
repeaterKey,
|
||||||
|
);
|
||||||
|
observations.add(observation);
|
||||||
|
|
||||||
|
// Keep at most 8 observations (most recent per repeater)
|
||||||
|
if (observations.length > 8) {
|
||||||
|
observations.sort((a, b) => b.observedAt.compareTo(a.observedAt));
|
||||||
|
observations.removeRange(8, observations.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
final estimated = RssiLocationEstimator.trilaterate(
|
||||||
|
observations: observations,
|
||||||
|
contactPublicKey: contactPublicKey,
|
||||||
|
);
|
||||||
|
if (estimated != null) {
|
||||||
|
_estimatedLocations[contactPublicKeyHex] = estimated;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Effective location: own GPS/advert > estimated from RSSI.
|
||||||
|
LatLng? effectiveLocationFor(Contact contact) {
|
||||||
|
return contact.displayLocation ?? _estimatedLocations[contact.publicKeyHex];
|
||||||
|
}
|
||||||
|
|
||||||
/// Get contacts with location (for map display)
|
/// Get contacts with location (for map display)
|
||||||
|
/// Includes contacts with estimated locations from RSSI.
|
||||||
List<Contact> get contactsWithLocation =>
|
List<Contact> get contactsWithLocation =>
|
||||||
contacts.where((c) => c.displayLocation != null).toList();
|
contacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||||
|
|
||||||
/// Get chat contacts with location (team members on map)
|
/// Get chat contacts with location (team members on map)
|
||||||
List<Contact> get chatContactsWithLocation =>
|
List<Contact> get chatContactsWithLocation =>
|
||||||
chatContacts.where((c) => c.displayLocation != null).toList();
|
chatContacts.where((c) => effectiveLocationFor(c) != null).toList();
|
||||||
|
|
||||||
MessageContactLocation? buildMessageContactLocationSnapshot(
|
MessageContactLocation? buildMessageContactLocationSnapshot(
|
||||||
Contact contact, {
|
Contact contact, {
|
||||||
@@ -541,7 +721,9 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if this is a new contact
|
// Check if this is a new contact
|
||||||
final existingContact = _contacts[contact.publicKeyHex];
|
final existingContact =
|
||||||
|
_contacts[contact.publicKeyHex] ??
|
||||||
|
_retainedContactsForSync[contact.publicKeyHex];
|
||||||
final isNewContact = existingContact == null;
|
final isNewContact = existingContact == null;
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' isNew: $isNewContact, total contacts before: ${_contacts.length}',
|
' isNew: $isNewContact, total contacts before: ${_contacts.length}',
|
||||||
@@ -553,6 +735,13 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
|
|
||||||
_contacts[contact.publicKeyHex] = updatedContact;
|
_contacts[contact.publicKeyHex] = updatedContact;
|
||||||
|
_retainedContactsForSync.remove(contact.publicKeyHex);
|
||||||
|
// Keep repeaters and sensors in pending adverts so they remain visible
|
||||||
|
// in the discovery list (with a checkmark). Remove others.
|
||||||
|
if (contact.type != ContactType.repeater &&
|
||||||
|
contact.type != ContactType.sensor) {
|
||||||
|
_pendingAdverts.remove(contact.publicKeyHex);
|
||||||
|
}
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
' ✅ Contact added/updated. Total contacts: ${_contacts.length}, channels: ${channels.length}',
|
||||||
);
|
);
|
||||||
@@ -576,11 +765,18 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
excluded++;
|
excluded++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final existingContact = _contacts[contact.publicKeyHex];
|
final existingContact =
|
||||||
|
_contacts[contact.publicKeyHex] ??
|
||||||
|
_retainedContactsForSync[contact.publicKeyHex];
|
||||||
_contacts[contact.publicKeyHex] = _mergeIncomingContact(
|
_contacts[contact.publicKeyHex] = _mergeIncomingContact(
|
||||||
incomingContact: contact,
|
incomingContact: contact,
|
||||||
existingContact: existingContact,
|
existingContact: existingContact,
|
||||||
);
|
);
|
||||||
|
_retainedContactsForSync.remove(contact.publicKeyHex);
|
||||||
|
if (contact.type != ContactType.repeater &&
|
||||||
|
contact.type != ContactType.sensor) {
|
||||||
|
_pendingAdverts.remove(contact.publicKeyHex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (excluded > 0) {
|
if (excluded > 0) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -820,6 +1016,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Update contact telemetry
|
/// Update contact telemetry
|
||||||
|
// Dedup: track last telemetry data hash per contact to avoid processing
|
||||||
|
// the same payload twice (0x8B + 0x8C can both fire for the same data).
|
||||||
|
final Map<String, (int, DateTime)> _lastTelemetryHash = {};
|
||||||
|
|
||||||
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
void updateTelemetry(Uint8List publicKeyPrefix, Uint8List lppData) {
|
||||||
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
|
debugPrint('📊 [ContactsProvider] updateTelemetry() called');
|
||||||
debugPrint(
|
debugPrint(
|
||||||
@@ -827,19 +1027,46 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
debugPrint(' LPP data size: ${lppData.length} bytes');
|
debugPrint(' LPP data size: ${lppData.length} bytes');
|
||||||
|
|
||||||
|
// Deduplicate: same data for same contact within 2 seconds = skip
|
||||||
|
final prefixHex = publicKeyPrefix
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join();
|
||||||
|
final dataHash = lppData.fold<int>(0, (h, b) => h * 31 + b);
|
||||||
|
final now = DateTime.now();
|
||||||
|
final last = _lastTelemetryHash[prefixHex];
|
||||||
|
if (last != null &&
|
||||||
|
last.$1 == dataHash &&
|
||||||
|
now.difference(last.$2).inSeconds < 2) {
|
||||||
|
debugPrint(' ⏭️ Duplicate telemetry payload, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_lastTelemetryHash[prefixHex] = (dataHash, now);
|
||||||
|
|
||||||
// Find contact by public key prefix
|
// Find contact by public key prefix
|
||||||
final contact = _findContactByPrefix(publicKeyPrefix);
|
final contact = _findContactByPrefix(publicKeyPrefix);
|
||||||
if (contact == null) {
|
final isSelfTelemetry =
|
||||||
|
contact == null && _matchesSelfPrefix(publicKeyPrefix);
|
||||||
|
if (contact == null && !isSelfTelemetry) {
|
||||||
debugPrint(' ❌ Contact not found for this prefix');
|
debugPrint(' ❌ Contact not found for this prefix');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
debugPrint(' ✅ Found contact: ${contact.advName}');
|
if (isSelfTelemetry) {
|
||||||
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
debugPrint(' ✅ Matched self telemetry response');
|
||||||
|
debugPrint(
|
||||||
|
' Old self telemetry timestamp: ${_selfTelemetry?.timestamp}',
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
debugPrint(' ✅ Found contact: ${contact!.advName}');
|
||||||
|
debugPrint(' Old telemetry timestamp: ${contact.telemetry?.timestamp}');
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Parse Cayenne LPP data
|
// Parse Cayenne LPP data
|
||||||
var telemetry = CayenneLppParser.parse(lppData);
|
var telemetry = _withRawTelemetryHex(
|
||||||
|
CayenneLppParser.parse(lppData),
|
||||||
|
lppData,
|
||||||
|
);
|
||||||
debugPrint(' ✅ Parsed new telemetry');
|
debugPrint(' ✅ Parsed new telemetry');
|
||||||
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
debugPrint(' New telemetry timestamp: ${telemetry.timestamp}');
|
||||||
|
|
||||||
@@ -858,7 +1085,9 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final previousTelemetry = contact.telemetry;
|
final previousTelemetry = isSelfTelemetry
|
||||||
|
? _selfTelemetry
|
||||||
|
: contact!.telemetry;
|
||||||
|
|
||||||
final mergedTelemetry = _mergeTelemetryForContact(
|
final mergedTelemetry = _mergeTelemetryForContact(
|
||||||
existingTelemetry: previousTelemetry,
|
existingTelemetry: previousTelemetry,
|
||||||
@@ -870,31 +1099,49 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
telemetry.gpsLocation,
|
telemetry.gpsLocation,
|
||||||
)) {
|
)) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
' ⚠️ Retaining last valid GPS. Incoming telemetry GPS is invalid/missing: $incomingGps',
|
' ⚠️ Incoming telemetry GPS is invalid/missing; keeping last known contact location as fallback: $incomingGps',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
telemetry = mergedTelemetry;
|
telemetry = ContactTelemetry(
|
||||||
|
gpsLocation: telemetry.gpsLocation,
|
||||||
|
batteryPercentage: mergedTelemetry.batteryPercentage,
|
||||||
|
batteryMilliVolts: mergedTelemetry.batteryMilliVolts,
|
||||||
|
temperature: mergedTelemetry.temperature,
|
||||||
|
timestamp: mergedTelemetry.timestamp,
|
||||||
|
humidity: mergedTelemetry.humidity,
|
||||||
|
pressure: mergedTelemetry.pressure,
|
||||||
|
extraSensorData: mergedTelemetry.extraSensorData,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isSelfTelemetry) {
|
||||||
|
_selfTelemetry = telemetry;
|
||||||
|
notifyListeners();
|
||||||
|
debugPrint(' ✅ Updated self telemetry');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvedContact = contact!;
|
||||||
|
|
||||||
// Update contact with new telemetry AND last seen time
|
// Update contact with new telemetry AND last seen time
|
||||||
// lastAdvert is Unix timestamp in seconds
|
// lastAdvert is Unix timestamp in seconds
|
||||||
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
|
final currentTimestamp = (DateTime.now().millisecondsSinceEpoch / 1000)
|
||||||
.round();
|
.round();
|
||||||
debugPrint(' Old lastAdvert: ${contact.lastAdvert}');
|
debugPrint(' Old lastAdvert: ${resolvedContact.lastAdvert}');
|
||||||
debugPrint(' New lastAdvert: $currentTimestamp');
|
debugPrint(' New lastAdvert: $currentTimestamp');
|
||||||
|
|
||||||
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
final persistedGps = _getValidGpsOrNull(telemetry.gpsLocation);
|
||||||
final updatedContact = contact.copyWith(
|
final updatedContact = resolvedContact.copyWith(
|
||||||
telemetry: telemetry,
|
telemetry: telemetry,
|
||||||
lastAdvert: currentTimestamp, // Update last seen time
|
lastAdvert: currentTimestamp, // Update last seen time
|
||||||
advLat: persistedGps != null
|
advLat: persistedGps != null
|
||||||
? _coordinateToAdvertMicrodegrees(persistedGps.latitude)
|
? _coordinateToAdvertMicrodegrees(persistedGps.latitude)
|
||||||
: contact.advLat,
|
: resolvedContact.advLat,
|
||||||
advLon: persistedGps != null
|
advLon: persistedGps != null
|
||||||
? _coordinateToAdvertMicrodegrees(persistedGps.longitude)
|
? _coordinateToAdvertMicrodegrees(persistedGps.longitude)
|
||||||
: contact.advLon,
|
: resolvedContact.advLon,
|
||||||
);
|
);
|
||||||
_contacts[contact.publicKeyHex] = updatedContact;
|
_contacts[resolvedContact.publicKeyHex] = updatedContact;
|
||||||
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
debugPrint(' ✅ Updated contact in map (with new lastAdvert)');
|
||||||
|
|
||||||
_persistContacts();
|
_persistContacts();
|
||||||
@@ -917,6 +1164,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The fast-GPS beacon carries no timestamp — stamp our own receive time.
|
||||||
|
final receivedAt = DateTime.now();
|
||||||
|
final receivedAtSeconds = receivedAt.millisecondsSinceEpoch ~/ 1000;
|
||||||
|
|
||||||
final updatedTelemetry = _mergeTelemetryForContact(
|
final updatedTelemetry = _mergeTelemetryForContact(
|
||||||
existingTelemetry: contact.telemetry,
|
existingTelemetry: contact.telemetry,
|
||||||
incomingTelemetry: ContactTelemetry(
|
incomingTelemetry: ContactTelemetry(
|
||||||
@@ -924,22 +1175,41 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
batteryPercentage: null,
|
batteryPercentage: null,
|
||||||
batteryMilliVolts: null,
|
batteryMilliVolts: null,
|
||||||
temperature: null,
|
temperature: null,
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
timestamp: receivedAt,
|
||||||
packet.timestampSeconds * 1000,
|
|
||||||
),
|
|
||||||
humidity: null,
|
humidity: null,
|
||||||
pressure: null,
|
pressure: null,
|
||||||
extraSensorData: null,
|
extraSensorData: null,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
debugPrint(
|
||||||
|
'📥 [ContactsProvider] Fast GPS received '
|
||||||
|
'sender=${packet.senderKey6} '
|
||||||
|
'contact=${contact.displayName} '
|
||||||
|
'contactKey=${contact.publicKeyHex} '
|
||||||
|
'lat=${packet.latitude} '
|
||||||
|
'lon=${packet.longitude} '
|
||||||
|
'speed=${packet.speedKmh}km/h',
|
||||||
|
);
|
||||||
|
|
||||||
final updatedContact = contact.copyWith(
|
final updatedContact = contact.copyWith(
|
||||||
telemetry: updatedTelemetry,
|
telemetry: updatedTelemetry,
|
||||||
lastAdvert: packet.timestampSeconds,
|
lastAdvert: receivedAtSeconds,
|
||||||
|
lastMod: receivedAtSeconds,
|
||||||
advLat: _coordinateToAdvertMicrodegrees(packet.latitude),
|
advLat: _coordinateToAdvertMicrodegrees(packet.latitude),
|
||||||
advLon: _coordinateToAdvertMicrodegrees(packet.longitude),
|
advLon: _coordinateToAdvertMicrodegrees(packet.longitude),
|
||||||
);
|
);
|
||||||
_contacts[contact.publicKeyHex] = updatedContact;
|
_contacts[contact.publicKeyHex] = updatedContact;
|
||||||
|
_estimatedLocations[contact.publicKeyHex] = LatLng(
|
||||||
|
packet.latitude,
|
||||||
|
packet.longitude,
|
||||||
|
);
|
||||||
|
debugPrint(
|
||||||
|
'✅ [ContactsProvider] Fast GPS applied '
|
||||||
|
'contact=${updatedContact.displayName} '
|
||||||
|
'contactKey=${updatedContact.publicKeyHex} '
|
||||||
|
'lastAdvert=${updatedContact.lastAdvert}',
|
||||||
|
);
|
||||||
_persistContacts();
|
_persistContacts();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
@@ -988,10 +1258,10 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
// last known reading for any field that is omitted in the incoming update.
|
// last known reading for any field that is omitted in the incoming update.
|
||||||
final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation);
|
final incomingGps = _getValidGpsOrNull(incomingTelemetry.gpsLocation);
|
||||||
final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation);
|
final previousGps = _getValidGpsOrNull(existingTelemetry?.gpsLocation);
|
||||||
final mergedExtraSensorData = <String, dynamic>{
|
final mergedExtraSensorData = _mergeExtraSensorData(
|
||||||
...?existingTelemetry?.extraSensorData,
|
existingTelemetry?.extraSensorData,
|
||||||
...?incomingTelemetry.extraSensorData,
|
incomingTelemetry.extraSensorData,
|
||||||
};
|
);
|
||||||
|
|
||||||
return ContactTelemetry(
|
return ContactTelemetry(
|
||||||
gpsLocation: incomingGps ?? previousGps,
|
gpsLocation: incomingGps ?? previousGps,
|
||||||
@@ -1012,16 +1282,88 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContactTelemetry _withRawTelemetryHex(
|
||||||
|
ContactTelemetry telemetry,
|
||||||
|
Uint8List lppData,
|
||||||
|
) {
|
||||||
|
final extraSensorData = <String, dynamic>{...?telemetry.extraSensorData};
|
||||||
|
extraSensorData[_rawTelemetryHexKey] = _bytesToHex(lppData);
|
||||||
|
return ContactTelemetry(
|
||||||
|
gpsLocation: telemetry.gpsLocation,
|
||||||
|
batteryPercentage: telemetry.batteryPercentage,
|
||||||
|
batteryMilliVolts: telemetry.batteryMilliVolts,
|
||||||
|
temperature: telemetry.temperature,
|
||||||
|
timestamp: telemetry.timestamp,
|
||||||
|
humidity: telemetry.humidity,
|
||||||
|
pressure: telemetry.pressure,
|
||||||
|
extraSensorData: extraSensorData,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _bytesToHex(Uint8List bytes) {
|
||||||
|
return bytes.map((b) => b.toRadixString(16).padLeft(2, '0')).join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> _mergeExtraSensorData(
|
||||||
|
Map<String, dynamic>? existingExtraSensorData,
|
||||||
|
Map<String, dynamic>? incomingExtraSensorData,
|
||||||
|
) {
|
||||||
|
if (incomingExtraSensorData == null || incomingExtraSensorData.isEmpty) {
|
||||||
|
return <String, dynamic>{...?existingExtraSensorData};
|
||||||
|
}
|
||||||
|
|
||||||
|
final isFullTelemetryRefresh = incomingExtraSensorData.containsKey(
|
||||||
|
_rawTelemetryHexKey,
|
||||||
|
);
|
||||||
|
final merged = <String, dynamic>{
|
||||||
|
if (!isFullTelemetryRefresh) ...?existingExtraSensorData,
|
||||||
|
};
|
||||||
|
|
||||||
|
final incomingMetricFamilies = incomingExtraSensorData.keys
|
||||||
|
.map(_telemetryMetricFamilyForKey)
|
||||||
|
.whereType<String>()
|
||||||
|
.toSet();
|
||||||
|
if (incomingMetricFamilies.isNotEmpty) {
|
||||||
|
merged.removeWhere((key, _) {
|
||||||
|
final family = _telemetryMetricFamilyForKey(key);
|
||||||
|
return family != null && incomingMetricFamilies.contains(family);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
merged.addAll(incomingExtraSensorData);
|
||||||
|
return merged;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _telemetryMetricFamilyForKey(String key) {
|
||||||
|
const sourcePrefix = '__source_channel:';
|
||||||
|
if (key.startsWith(sourcePrefix)) {
|
||||||
|
return key.substring(sourcePrefix.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
final separatorIndex = key.lastIndexOf('_');
|
||||||
|
if (separatorIndex <= 0 || separatorIndex == key.length - 1) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
final suffix = key.substring(separatorIndex + 1);
|
||||||
|
if (int.tryParse(suffix) == null) {
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
return key.substring(0, separatorIndex);
|
||||||
|
}
|
||||||
|
|
||||||
int _coordinateToAdvertMicrodegrees(double coordinate) {
|
int _coordinateToAdvertMicrodegrees(double coordinate) {
|
||||||
return (coordinate * 1e6).round();
|
return (coordinate * 1e6).round();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find contact by public key prefix (6 bytes)
|
/// Find contact by public key prefix (6 bytes)
|
||||||
Contact? _findContactByPrefix(Uint8List prefix) {
|
Contact? _findContactByPrefix(Uint8List prefix) {
|
||||||
if (prefix.length < 6) return null;
|
if (prefix.isEmpty) return null;
|
||||||
|
|
||||||
|
final takeLen = prefix.length < 6 ? prefix.length : 6;
|
||||||
final prefixHex = prefix
|
final prefixHex = prefix
|
||||||
.sublist(0, 6)
|
.sublist(0, takeLen)
|
||||||
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
.join('');
|
.join('');
|
||||||
|
|
||||||
@@ -1057,6 +1399,36 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return _contacts[keyHex];
|
return _contacts[keyHex];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _setSelfDevicePublicKey(Uint8List? devicePublicKey) {
|
||||||
|
final nextKeyHex = _publicKeyHexOrNull(devicePublicKey);
|
||||||
|
if (_selfPublicKeyHex != nextKeyHex) {
|
||||||
|
_selfTelemetry = null;
|
||||||
|
}
|
||||||
|
_selfPublicKeyHex = nextKeyHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _publicKeyHexOrNull(Uint8List? publicKey) {
|
||||||
|
if (publicKey == null || publicKey.isEmpty) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return publicKey.map((b) => b.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesSelfPrefix(Uint8List prefix) {
|
||||||
|
final selfKeyHex = _selfPublicKeyHex;
|
||||||
|
if (selfKeyHex == null || prefix.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final takeLen = prefix.length < 6 ? prefix.length : 6;
|
||||||
|
final prefixHex = prefix
|
||||||
|
.sublist(0, takeLen)
|
||||||
|
.map((b) => b.toRadixString(16).padLeft(2, '0'))
|
||||||
|
.join('');
|
||||||
|
return selfKeyHex.startsWith(prefixHex);
|
||||||
|
}
|
||||||
|
|
||||||
/// Clear a contact's learned path locally so the UI and next send both
|
/// Clear a contact's learned path locally so the UI and next send both
|
||||||
/// prefer flood routing until the radio reports a fresh route.
|
/// prefer flood routing until the radio reports a fresh route.
|
||||||
void markPathUnhealthy(Uint8List publicKey) {
|
void markPathUnhealthy(Uint8List publicKey) {
|
||||||
@@ -1354,6 +1726,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
void clearContacts() {
|
void clearContacts() {
|
||||||
_contacts.clear();
|
_contacts.clear();
|
||||||
_pendingAdverts.clear();
|
_pendingAdverts.clear();
|
||||||
|
_retainedContactsForSync.clear();
|
||||||
_ensurePublicChannelExists();
|
_ensurePublicChannelExists();
|
||||||
_persistContacts();
|
_persistContacts();
|
||||||
_persistPendingAdverts();
|
_persistPendingAdverts();
|
||||||
@@ -1378,6 +1751,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
// Then remove from local storage
|
// Then remove from local storage
|
||||||
_contacts.remove(publicKeyHex);
|
_contacts.remove(publicKeyHex);
|
||||||
_pendingAdverts.remove(publicKeyHex);
|
_pendingAdverts.remove(publicKeyHex);
|
||||||
|
_retainedContactsForSync.remove(publicKeyHex);
|
||||||
_persistContacts();
|
_persistContacts();
|
||||||
_persistPendingAdverts();
|
_persistPendingAdverts();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
@@ -1385,11 +1759,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
|
|
||||||
/// Get storage statistics
|
/// Get storage statistics
|
||||||
Future<Map<String, dynamic>> getStorageStats() async {
|
Future<Map<String, dynamic>> getStorageStats() async {
|
||||||
return await _storageService.getStorageStats();
|
return await _storageService.getStorageStats(namespace: _storageNamespace);
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> persistNow() async {
|
|
||||||
await _storageService.saveContacts(_contactsForStorage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> clearPendingAdverts() async {
|
Future<void> clearPendingAdverts() async {
|
||||||
@@ -1397,7 +1767,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
_pendingAdverts.clear();
|
_pendingAdverts.clear();
|
||||||
await _storageService.clearPendingAdverts();
|
await _storageService.clearPendingAdverts(namespace: _storageNamespace);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1460,6 +1830,16 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _resetInMemoryState() {
|
||||||
|
_contacts.clear();
|
||||||
|
_savedContactGroups.clear();
|
||||||
|
_pendingAdverts.clear();
|
||||||
|
_estimatedLocations.clear();
|
||||||
|
_rssiObservations.clear();
|
||||||
|
_retainedContactsForSync.clear();
|
||||||
|
_selfTelemetry = null;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
|
Map<String, dynamic> _pendingAdvertToJson(PendingAdvert advert) {
|
||||||
return {
|
return {
|
||||||
'publicKey': base64Encode(advert.publicKey),
|
'publicKey': base64Encode(advert.publicKey),
|
||||||
@@ -1526,6 +1906,7 @@ class ContactsProvider with ChangeNotifier {
|
|||||||
return {
|
return {
|
||||||
'chat': chatContacts.length,
|
'chat': chatContacts.length,
|
||||||
'repeater': repeaters.length,
|
'repeater': repeaters.length,
|
||||||
|
'sensor': sensorContacts.length,
|
||||||
'room': rooms.length,
|
'room': rooms.length,
|
||||||
'total': contacts.length,
|
'total': contacts.length,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -33,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);
|
||||||
@@ -48,8 +57,19 @@ 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;
|
||||||
@@ -66,6 +86,35 @@ class DrawingProvider with ChangeNotifier {
|
|||||||
_isInitialized = true;
|
_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
|
||||||
void setDrawingMode(DrawingMode mode) {
|
void setDrawingMode(DrawingMode mode) {
|
||||||
if (_drawingMode != mode) {
|
if (_drawingMode != mode) {
|
||||||
@@ -98,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
|
||||||
@@ -137,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);
|
||||||
@@ -180,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();
|
||||||
}
|
}
|
||||||
@@ -216,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);
|
||||||
@@ -237,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);
|
||||||
@@ -246,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);
|
||||||
}
|
}
|
||||||
@@ -302,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');
|
||||||
}
|
}
|
||||||
@@ -312,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();
|
||||||
@@ -339,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) {
|
||||||
@@ -376,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(
|
||||||
@@ -390,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;
|
||||||
@@ -404,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
|
||||||
@@ -428,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(
|
||||||
@@ -442,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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,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(
|
||||||
@@ -483,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,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.
|
||||||
|
/// Sequence: 2 direct attempts, flood, then last successful route.
|
||||||
|
static const int maxRetryAttemptsWithPath = 3;
|
||||||
|
|
||||||
|
/// 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
|
||||||
|
|||||||
@@ -9,6 +9,11 @@ typedef RawPacketSender =
|
|||||||
required Uint8List payload,
|
required Uint8List payload,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/// Pacing between consecutive raw fragments. Firmware v1.15 rejects bursts of
|
||||||
|
/// raw packets with `ERROR: Table full` when fragments are blasted too quickly,
|
||||||
|
/// so we space them out to keep the radio's raw transmit queue from overflowing.
|
||||||
|
const Duration _interFragmentDelay = Duration(milliseconds: 350);
|
||||||
|
|
||||||
Future<bool> serveCachedSessionFragments<T>({
|
Future<bool> serveCachedSessionFragments<T>({
|
||||||
required String providerLabel,
|
required String providerLabel,
|
||||||
required String sessionId,
|
required String sessionId,
|
||||||
@@ -19,6 +24,7 @@ Future<bool> serveCachedSessionFragments<T>({
|
|||||||
required Uint8List Function(T fragment) encodeBinary,
|
required Uint8List Function(T fragment) encodeBinary,
|
||||||
required RawPacketSender? sendRawPacket,
|
required RawPacketSender? sendRawPacket,
|
||||||
Set<int>? requestedIndices,
|
Set<int>? requestedIndices,
|
||||||
|
Duration interFragmentDelay = _interFragmentDelay,
|
||||||
}) async {
|
}) async {
|
||||||
if (fragments.isEmpty) {
|
if (fragments.isEmpty) {
|
||||||
debugPrint('⚠️ [$providerLabel] No cached fragments for $sessionId');
|
debugPrint('⚠️ [$providerLabel] No cached fragments for $sessionId');
|
||||||
@@ -56,6 +62,9 @@ Future<bool> serveCachedSessionFragments<T>({
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
|
if (servedCount > 0 && interFragmentDelay > Duration.zero) {
|
||||||
|
await Future<void>.delayed(interFragmentDelay);
|
||||||
|
}
|
||||||
await sendRawPacket(
|
await sendRawPacket(
|
||||||
contactPath: requester.outPath,
|
contactPath: requester.outPath,
|
||||||
contactPathLen: requester.routeEncodedPathLen,
|
contactPathLen: requester.routeEncodedPathLen,
|
||||||
|
|||||||
@@ -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,47 @@
|
|||||||
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;
|
||||||
|
bool _isTrailRecordingEnabled = 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 +54,28 @@ 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;
|
||||||
|
bool get isTrailRecordingEnabled => _isTrailRecordingEnabled;
|
||||||
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 +88,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 +235,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 +361,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 +374,26 @@ 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 (!_isTrailRecordingEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
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 +406,29 @@ class MapProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle trail visibility
|
|
||||||
void toggleTrailVisibility() {
|
void toggleTrailVisibility() {
|
||||||
_isTrailVisible = !_isTrailVisible;
|
_isTrailVisible = !_isTrailVisible;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clear the current trail
|
void setTrailRecordingEnabled(bool enabled) {
|
||||||
|
if (_isTrailRecordingEnabled == enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTrailRecordingEnabled = enabled;
|
||||||
|
if (!enabled) {
|
||||||
|
if (_currentTrail != null) {
|
||||||
|
endTrail();
|
||||||
|
} else {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
void clearCurrentTrail() {
|
void clearCurrentTrail() {
|
||||||
if (_currentTrail != null) {
|
if (_currentTrail != null) {
|
||||||
_currentTrail = null;
|
_currentTrail = null;
|
||||||
@@ -245,158 +436,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 +614,169 @@ 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,
|
||||||
|
'isTrailRecordingEnabled': _isTrailRecordingEnabled,
|
||||||
|
'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;
|
||||||
|
_isTrailRecordingEnabled =
|
||||||
|
json['isTrailRecordingEnabled'] 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import '../models/message.dart';
|
import '../models/message.dart';
|
||||||
import '../models/contact.dart';
|
import '../models/contact.dart';
|
||||||
import '../models/message_contact_location.dart';
|
import '../models/message_contact_location.dart';
|
||||||
@@ -10,6 +11,7 @@ import '../models/path_selection.dart';
|
|||||||
import '../models/sar_marker.dart';
|
import '../models/sar_marker.dart';
|
||||||
import '../models/map_drawing.dart';
|
import '../models/map_drawing.dart';
|
||||||
import '../services/message_storage_service.dart';
|
import '../services/message_storage_service.dart';
|
||||||
|
import '../services/location_tracking_service.dart';
|
||||||
import '../services/notification_service.dart';
|
import '../services/notification_service.dart';
|
||||||
import '../utils/sar_message_parser.dart';
|
import '../utils/sar_message_parser.dart';
|
||||||
import '../utils/drawing_message_parser.dart';
|
import '../utils/drawing_message_parser.dart';
|
||||||
@@ -23,6 +25,8 @@ 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);
|
||||||
|
static const Duration _receivedDuplicateWindow = Duration(seconds: 5);
|
||||||
|
static const Duration _deliveredRouteRefreshWindow = Duration(seconds: 30);
|
||||||
|
|
||||||
final List<Message> _messages = [];
|
final List<Message> _messages = [];
|
||||||
final Map<String, SarMarker> _sarMarkers = {};
|
final Map<String, SarMarker> _sarMarkers = {};
|
||||||
@@ -32,11 +36,16 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
bool _isInitialized = false;
|
bool _isInitialized = false;
|
||||||
bool _isPersisting = false;
|
bool _isPersisting = false;
|
||||||
bool _persistRequested = false;
|
bool _persistRequested = false;
|
||||||
|
bool _suppressReceivedNotifications = 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 = {};
|
||||||
|
final Map<String, List<_PendingDeliveredRouteRefresh>>
|
||||||
|
_pendingDeliveredRouteRefreshByContact =
|
||||||
|
<String, List<_PendingDeliveredRouteRefresh>>{};
|
||||||
|
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 = {};
|
||||||
@@ -95,6 +104,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,
|
||||||
@@ -149,6 +162,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;
|
||||||
|
|
||||||
@@ -185,6 +199,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messages[index] = _messages[index].copyWith(
|
_messages[index] = _messages[index].copyWith(
|
||||||
usedFloodFallback: selection.usesFlood,
|
usedFloodFallback: selection.usesFlood,
|
||||||
pathLen: nextPathLen,
|
pathLen: nextPathLen,
|
||||||
|
pathBytes: selection.hasDirectPath
|
||||||
|
? Uint8List.fromList(selection.pathBytes)
|
||||||
|
: Uint8List(0),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,6 +209,83 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void queueDeliveredMessageRouteRefresh(String messageId, Contact contact) {
|
||||||
|
if (messageId.isEmpty || contact.publicKeyHex.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_prunePendingDeliveredRouteRefresh();
|
||||||
|
final queue = _pendingDeliveredRouteRefreshByContact.putIfAbsent(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
() => <_PendingDeliveredRouteRefresh>[],
|
||||||
|
);
|
||||||
|
queue.removeWhere((entry) => entry.messageId == messageId);
|
||||||
|
queue.add(
|
||||||
|
_PendingDeliveredRouteRefresh(
|
||||||
|
messageId: messageId,
|
||||||
|
queuedAt: DateTime.now(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool applyDeliveredMessageRouteFromContact(Contact contact) {
|
||||||
|
if (!contact.routeHasPath ||
|
||||||
|
contact.routeHopCount <= 0 ||
|
||||||
|
contact.publicKeyHex.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_prunePendingDeliveredRouteRefresh();
|
||||||
|
final queue = _pendingDeliveredRouteRefreshByContact[contact.publicKeyHex];
|
||||||
|
if (queue == null || queue.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (queue.isNotEmpty) {
|
||||||
|
final pending = queue.removeAt(0);
|
||||||
|
final index = _messages.indexWhere(
|
||||||
|
(message) => message.id == pending.messageId,
|
||||||
|
);
|
||||||
|
if (index == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final message = _messages[index];
|
||||||
|
if (!message.isContactMessage ||
|
||||||
|
message.deliveryStatus != MessageDeliveryStatus.delivered) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final existingMetadata = _messageRouteMetadata[pending.messageId];
|
||||||
|
_messageRouteMetadata[pending.messageId] = MessageRouteMetadata(
|
||||||
|
mode: PathSelectionMode.directCurrent,
|
||||||
|
routerFallbackAttempted:
|
||||||
|
existingMetadata?.routerFallbackAttempted ?? false,
|
||||||
|
canonicalPath: contact.routeCanonicalText.isEmpty
|
||||||
|
? null
|
||||||
|
: contact.routeCanonicalText,
|
||||||
|
hopCount: contact.routeHopCount,
|
||||||
|
);
|
||||||
|
_messages[index] = message.copyWith(
|
||||||
|
pathLen: contact.routeHopCount,
|
||||||
|
usedFloodFallback: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (queue.isEmpty) {
|
||||||
|
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
|
||||||
|
}
|
||||||
|
|
||||||
|
_persistMessages();
|
||||||
|
notifyListeners();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue.isEmpty) {
|
||||||
|
_pendingDeliveredRouteRefreshByContact.remove(contact.publicKeyHex);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/// Set localizations for notifications
|
/// Set localizations for notifications
|
||||||
void setLocalizations(AppLocalizations localizations) {
|
void setLocalizations(AppLocalizations localizations) {
|
||||||
_localizations = localizations;
|
_localizations = localizations;
|
||||||
@@ -311,36 +405,70 @@ 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
|
|
||||||
..clear()
|
|
||||||
..addAll(storedContactLocations);
|
|
||||||
_messageReceptionDetails
|
|
||||||
..clear()
|
|
||||||
..addAll(storedReceptionDetails);
|
|
||||||
_messageTransferDetails
|
|
||||||
..clear()
|
|
||||||
..addAll(storedTransferDetails);
|
|
||||||
_messageRouteMetadata
|
|
||||||
..clear()
|
|
||||||
..addAll(storedRouteMetadata);
|
|
||||||
_removedSarMarkerIds
|
_removedSarMarkerIds
|
||||||
..clear()
|
..clear()
|
||||||
..addAll(storedRemovedSarMarkerIds);
|
..addAll(storedRemovedSarMarkerIds);
|
||||||
|
|
||||||
// Add stored messages with enhancement to ensure SAR detection
|
// Rebuild the in-memory list through the normal duplicate rules so
|
||||||
|
// stale duplicate rows already persisted on-device are collapsed.
|
||||||
for (final message in storedMessages) {
|
for (final message in storedMessages) {
|
||||||
// Re-enhance each message to ensure SAR markers are properly detected
|
// Re-enhance each message to ensure SAR markers are properly detected
|
||||||
// This handles cases where messages were stored before enhancement logic
|
// This handles cases where messages were stored before enhancement logic
|
||||||
@@ -381,7 +509,66 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||||
|
|
||||||
|
final duplicateIndex = _findDuplicateMessageIndex(enhancedMessage);
|
||||||
|
if (duplicateIndex != -1) {
|
||||||
|
final existingId = _messages[duplicateIndex].id;
|
||||||
|
final incomingReceptionDetails = storedReceptionDetails[message.id];
|
||||||
|
final existingMessage = _messages[duplicateIndex];
|
||||||
|
final duplicatePathBytes = incomingReceptionDetails?.pathBytes;
|
||||||
|
|
||||||
|
final storedLocation = storedContactLocations[message.id];
|
||||||
|
if (storedLocation != null &&
|
||||||
|
!_messageContactLocations.containsKey(existingId)) {
|
||||||
|
_messageContactLocations[existingId] = storedLocation;
|
||||||
|
}
|
||||||
|
|
||||||
|
_messageReceptionDetails[existingId] =
|
||||||
|
MessageReceptionDetails.mergeDuplicate(
|
||||||
|
existing: _messageReceptionDetails[existingId],
|
||||||
|
incoming: incomingReceptionDetails,
|
||||||
|
);
|
||||||
|
|
||||||
|
final storedTransferDetailsForMessage = storedTransferDetails[message.id];
|
||||||
|
if (storedTransferDetailsForMessage != null &&
|
||||||
|
!_messageTransferDetails.containsKey(existingId)) {
|
||||||
|
_messageTransferDetails[existingId] = storedTransferDetailsForMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
final storedRouteMetadataForMessage = storedRouteMetadata[message.id];
|
||||||
|
if (storedRouteMetadataForMessage != null &&
|
||||||
|
!_messageRouteMetadata.containsKey(existingId)) {
|
||||||
|
_messageRouteMetadata[existingId] = storedRouteMetadataForMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingMessage.pathBytes == null &&
|
||||||
|
duplicatePathBytes != null &&
|
||||||
|
duplicatePathBytes.isNotEmpty) {
|
||||||
|
_messages[duplicateIndex] = existingMessage.copyWith(
|
||||||
|
pathBytes: Uint8List.fromList(duplicatePathBytes),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
_messages.add(enhancedMessage);
|
_messages.add(enhancedMessage);
|
||||||
|
final storedLocation = storedContactLocations[message.id];
|
||||||
|
if (storedLocation != null) {
|
||||||
|
_messageContactLocations[message.id] = storedLocation;
|
||||||
|
}
|
||||||
|
final storedReception = storedReceptionDetails[message.id];
|
||||||
|
if (storedReception != null) {
|
||||||
|
_messageReceptionDetails[message.id] = storedReception;
|
||||||
|
}
|
||||||
|
final storedTransfer = storedTransferDetails[message.id];
|
||||||
|
if (storedTransfer != null) {
|
||||||
|
_messageTransferDetails[message.id] = storedTransfer;
|
||||||
|
}
|
||||||
|
final storedRoute = storedRouteMetadata[message.id];
|
||||||
|
if (storedRoute != null) {
|
||||||
|
_messageRouteMetadata[message.id] = storedRoute;
|
||||||
|
}
|
||||||
|
|
||||||
// Extract SAR markers
|
// Extract SAR markers
|
||||||
if (enhancedMessage.isSarMarker) {
|
if (enhancedMessage.isSarMarker) {
|
||||||
@@ -613,6 +800,42 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return; // Skip duplicate
|
return; // Skip duplicate
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final matchingSentReplayIndex = _findMatchingSentReplayIndex(finalMessage);
|
||||||
|
if (matchingSentReplayIndex != -1) {
|
||||||
|
final existingId = _messages[matchingSentReplayIndex].id;
|
||||||
|
_clearChannelSendWarning(existingId);
|
||||||
|
if (contactLocationSnapshot != null) {
|
||||||
|
_messageContactLocations[existingId] = contactLocationSnapshot;
|
||||||
|
}
|
||||||
|
final mergedReceptionDetails = MessageReceptionDetails.mergeDuplicate(
|
||||||
|
existing: _messageReceptionDetails[existingId],
|
||||||
|
incoming: receptionDetailsSnapshot,
|
||||||
|
);
|
||||||
|
_messageReceptionDetails[existingId] = mergedReceptionDetails;
|
||||||
|
final existingMessage = _messages[matchingSentReplayIndex];
|
||||||
|
final routeMetadata = _messageRouteMetadata[existingId];
|
||||||
|
_messages[matchingSentReplayIndex] = existingMessage.copyWith(
|
||||||
|
echoCount: _mergeSentReplayEchoCount(
|
||||||
|
existingMessage,
|
||||||
|
mergedReceptionDetails,
|
||||||
|
),
|
||||||
|
pathLen: _mergeSentReplayPathLen(
|
||||||
|
existingMessage,
|
||||||
|
finalMessage,
|
||||||
|
routeMetadata,
|
||||||
|
),
|
||||||
|
pathBytes: _mergeSentReplayPathBytes(
|
||||||
|
existingMessage,
|
||||||
|
finalMessage,
|
||||||
|
routeMetadata,
|
||||||
|
),
|
||||||
|
firstEchoAt: existingMessage.firstEchoAt ?? DateTime.now(),
|
||||||
|
lastEchoAt: DateTime.now(),
|
||||||
|
);
|
||||||
|
_persistMessages();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_messages.add(finalMessage);
|
_messages.add(finalMessage);
|
||||||
if (contactLocationSnapshot != null) {
|
if (contactLocationSnapshot != null) {
|
||||||
_messageContactLocations[finalMessage.id] = contactLocationSnapshot;
|
_messageContactLocations[finalMessage.id] = contactLocationSnapshot;
|
||||||
@@ -628,11 +851,13 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_sarMarkers[marker.id] = marker;
|
_sarMarkers[marker.id] = marker;
|
||||||
|
|
||||||
// Trigger urgent notification for received SAR messages (not sent by user)
|
// Trigger urgent notification for received SAR messages (not sent by user)
|
||||||
if (!finalMessage.isSentMessage) {
|
if (!finalMessage.isSentMessage && !_suppressReceivedNotifications) {
|
||||||
_triggerSarNotification(finalMessage, marker);
|
_triggerSarNotification(finalMessage, marker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!finalMessage.isSentMessage && !finalMessage.isSystemMessage) {
|
} else if (!finalMessage.isSentMessage &&
|
||||||
|
!finalMessage.isSystemMessage &&
|
||||||
|
!_suppressReceivedNotifications) {
|
||||||
// Trigger notification for regular messages (not SAR, not sent by user, not system)
|
// Trigger notification for regular messages (not SAR, not sent by user, not system)
|
||||||
_triggerMessageNotification(finalMessage);
|
_triggerMessageNotification(finalMessage);
|
||||||
}
|
}
|
||||||
@@ -643,6 +868,18 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<T> withReceivedNotificationsSuppressed<T>(
|
||||||
|
Future<T> Function() action,
|
||||||
|
) async {
|
||||||
|
final previous = _suppressReceivedNotifications;
|
||||||
|
_suppressReceivedNotifications = true;
|
||||||
|
try {
|
||||||
|
return await action();
|
||||||
|
} finally {
|
||||||
|
_suppressReceivedNotifications = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a message is a duplicate
|
/// Check if a message is a duplicate
|
||||||
///
|
///
|
||||||
/// Messages are considered duplicates if they have:
|
/// Messages are considered duplicates if they have:
|
||||||
@@ -659,10 +896,27 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final exactDuplicateIndex = _findExactDuplicateMessageIndex(message);
|
||||||
|
if (exactDuplicateIndex != -1) {
|
||||||
|
return exactDuplicateIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastConversationDuplicateIndex =
|
||||||
|
_findLastConversationDuplicateMessageIndex(message);
|
||||||
|
if (lastConversationDuplicateIndex != -1) {
|
||||||
|
return lastConversationDuplicateIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _findExactDuplicateMessageIndex(Message message) {
|
||||||
|
final normalizedIncomingText = _normalizedDuplicateText(message.text);
|
||||||
for (int index = 0; index < _messages.length; index++) {
|
for (int index = 0; index < _messages.length; index++) {
|
||||||
final existing = _messages[index];
|
final existing = _messages[index];
|
||||||
if (!_matchesDuplicateScope(existing, message) ||
|
if (existing.isSentMessage ||
|
||||||
existing.text != message.text) {
|
!_matchesExactDuplicateScope(existing, message) ||
|
||||||
|
_normalizedDuplicateText(existing.text) != normalizedIncomingText) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (message.isChannelMessage) {
|
if (message.isChannelMessage) {
|
||||||
@@ -678,13 +932,42 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool _matchesDuplicateScope(Message existing, Message message) {
|
int _findLastConversationDuplicateMessageIndex(Message message) {
|
||||||
|
final normalizedIncomingText = _normalizedDuplicateText(message.text);
|
||||||
|
for (int index = _messages.length - 1; index >= 0; index--) {
|
||||||
|
final existing = _messages[index];
|
||||||
|
if (!_isSameConversation(existing, message)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
final receivedDelta = existing.receivedAt.difference(message.receivedAt).abs();
|
||||||
|
if (receivedDelta > _receivedDuplicateWindow) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (existing.isSentMessage || existing.isSystemMessage) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_normalizedDuplicateText(existing.text) != normalizedIncomingText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (_matchesDuplicateSenderIdentity(existing, message)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesExactDuplicateScope(Message existing, Message message) {
|
||||||
if (existing.messageType != message.messageType) {
|
if (existing.messageType != message.messageType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.isContactMessage) {
|
if (message.isContactMessage) {
|
||||||
return existing.senderKeyShort == message.senderKeyShort;
|
if (!_isSameConversation(existing, message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return existing.senderTimestamp == message.senderTimestamp &&
|
||||||
|
_matchesDuplicateSenderIdentity(existing, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (message.isChannelMessage) {
|
if (message.isChannelMessage) {
|
||||||
@@ -692,52 +975,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final withinChannelRepeatWindow =
|
if (existing.senderTimestamp == message.senderTimestamp) {
|
||||||
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
|
||||||
|
|
||||||
final existingSenderKey = existing.senderKeyShort;
|
|
||||||
final incomingSenderKey = message.senderKeyShort;
|
|
||||||
if (existingSenderKey != null &&
|
|
||||||
incomingSenderKey != null &&
|
|
||||||
existingSenderKey == incomingSenderKey &&
|
|
||||||
withinChannelRepeatWindow) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final existingSenderName = _normalizedResolvedSenderName(existing);
|
|
||||||
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 withinChannelRepeatWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -745,6 +985,131 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _isSameConversation(Message existing, Message message) {
|
||||||
|
if (existing.messageType != message.messageType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.isChannelMessage) {
|
||||||
|
return existing.channelIdx == message.channelIdx;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message.isContactMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.recipientPublicKey != null && message.recipientPublicKey != null) {
|
||||||
|
return _listEquals(existing.recipientPublicKey!, message.recipientPublicKey!);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.recipientPublicKey == null && message.recipientPublicKey == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _findMatchingSentReplayIndex(Message message) {
|
||||||
|
if (!message.isChannelMessage || message.isSentMessage) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final normalizedIncomingText = _normalizedDuplicateText(message.text);
|
||||||
|
for (int index = 0; index < _messages.length; index++) {
|
||||||
|
final existing = _messages[index];
|
||||||
|
if (!existing.isSentMessage ||
|
||||||
|
!existing.isChannelMessage ||
|
||||||
|
_normalizedDuplicateText(existing.text) != normalizedIncomingText) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_matchesSentReplayScope(existing, message)) {
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesSentReplayScope(Message existing, Message message) {
|
||||||
|
if (!_isSameConversation(existing, message)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.senderTimestamp == message.senderTimestamp) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final withinChannelRepeatWindow =
|
||||||
|
(existing.senderTimestamp - message.senderTimestamp).abs() <= 30;
|
||||||
|
if (!withinChannelRepeatWindow) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _matchesDuplicateSenderIdentity(existing, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
int _mergeSentReplayEchoCount(
|
||||||
|
Message existing,
|
||||||
|
MessageReceptionDetails mergedReceptionDetails,
|
||||||
|
) {
|
||||||
|
final replayCount = mergedReceptionDetails.receivedCopies > 0
|
||||||
|
? mergedReceptionDetails.receivedCopies - 1
|
||||||
|
: 0;
|
||||||
|
return replayCount > existing.echoCount ? replayCount : existing.echoCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
int _mergeSentReplayPathLen(
|
||||||
|
Message existing,
|
||||||
|
Message incoming,
|
||||||
|
MessageRouteMetadata? routeMetadata,
|
||||||
|
) {
|
||||||
|
if (routeMetadata?.mode == PathSelectionMode.flood ||
|
||||||
|
existing.usedFloodFallback) {
|
||||||
|
return existing.pathLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
final routeHopCount = routeMetadata?.hopCount;
|
||||||
|
if (routeHopCount != null && routeHopCount > 0) {
|
||||||
|
return routeHopCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existing.pathLen > 0) {
|
||||||
|
return existing.pathLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
return incoming.pathLen > 0 ? incoming.pathLen : existing.pathLen;
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List? _mergeSentReplayPathBytes(
|
||||||
|
Message existing,
|
||||||
|
Message incoming,
|
||||||
|
MessageRouteMetadata? routeMetadata,
|
||||||
|
) {
|
||||||
|
if (routeMetadata?.mode == PathSelectionMode.flood ||
|
||||||
|
existing.usedFloodFallback) {
|
||||||
|
return existing.pathBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return existing.pathBytes ?? incoming.pathBytes;
|
||||||
|
}
|
||||||
|
|
||||||
/// Add multiple messages
|
/// Add multiple messages
|
||||||
void addMessages(List<Message> messages) {
|
void addMessages(List<Message> messages) {
|
||||||
int addedCount = 0;
|
int addedCount = 0;
|
||||||
@@ -858,6 +1223,10 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return _normalizeSenderName(resolved);
|
return _normalizeSenderName(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _normalizedDuplicateText(String value) {
|
||||||
|
return value.replaceAll('\r\n', '\n').trim();
|
||||||
|
}
|
||||||
|
|
||||||
void _scheduleChannelEchoWarning(String messageId) {
|
void _scheduleChannelEchoWarning(String messageId) {
|
||||||
_channelEchoWarningTimers[messageId]?.cancel();
|
_channelEchoWarningTimers[messageId]?.cancel();
|
||||||
_channelEchoWarningMessageIds.remove(messageId);
|
_channelEchoWarningMessageIds.remove(messageId);
|
||||||
@@ -1051,6 +1420,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
messageReceptionDetails: _messageReceptionDetails,
|
messageReceptionDetails: _messageReceptionDetails,
|
||||||
messageTransferDetails: _messageTransferDetails,
|
messageTransferDetails: _messageTransferDetails,
|
||||||
messageRouteMetadata: _messageRouteMetadata,
|
messageRouteMetadata: _messageRouteMetadata,
|
||||||
|
namespace: _storageNamespace,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1141,9 +1511,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
for (final message in messages) {
|
for (final message in messages) {
|
||||||
final occurrenceCount = _messageOccurrenceCount(message);
|
final occurrenceCount = _messageOccurrenceCount(message);
|
||||||
final existingIndex = entries.indexWhere(
|
final existingIndex = entries.indexWhere(
|
||||||
(entry) =>
|
(entry) => _shouldCollapseDisplayMessage(entry.message, message),
|
||||||
entry.message.text == message.text &&
|
|
||||||
_matchesDuplicateScope(entry.message, message),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (existingIndex == -1) {
|
if (existingIndex == -1) {
|
||||||
@@ -1161,6 +1529,20 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool _shouldCollapseDisplayMessage(Message existing, Message message) {
|
||||||
|
if (existing.isSentMessage || message.isSentMessage) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final receivedDelta = existing.receivedAt.difference(message.receivedAt).abs();
|
||||||
|
return _normalizedDuplicateText(existing.text) ==
|
||||||
|
_normalizedDuplicateText(message.text) &&
|
||||||
|
(_matchesExactDuplicateScope(existing, message) ||
|
||||||
|
(receivedDelta <= _receivedDuplicateWindow &&
|
||||||
|
_isSameConversation(existing, message) &&
|
||||||
|
_matchesDuplicateSenderIdentity(existing, message)));
|
||||||
|
}
|
||||||
|
|
||||||
int _messageOccurrenceCount(Message message) =>
|
int _messageOccurrenceCount(Message message) =>
|
||||||
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
_messageReceptionDetails[message.id]?.receivedCopies ?? 1;
|
||||||
|
|
||||||
@@ -1199,7 +1581,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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1324,7 +1709,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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1348,23 +1738,15 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
/// Clear all messages
|
/// Clear all messages
|
||||||
void clearMessages() {
|
void clearMessages() {
|
||||||
_cancelAllTimers();
|
_cancelAllTimers();
|
||||||
_messages.clear();
|
_resetInMemoryState();
|
||||||
_sarMarkers.clear();
|
|
||||||
_removedSarMarkerIds.clear();
|
|
||||||
_messageContactLocations.clear();
|
|
||||||
_messageReceptionDetails.clear();
|
|
||||||
_messageTransferDetails.clear();
|
|
||||||
_messageRouteMetadata.clear();
|
|
||||||
_pendingSentMessages.clear();
|
|
||||||
_messageContactMap.clear();
|
|
||||||
_groupedMessageMapping.clear();
|
|
||||||
_ackTagToRecipients.clear();
|
|
||||||
_messageAckHistory.clear();
|
|
||||||
_ackHistoryLookup.clear();
|
|
||||||
_completedAckHistory.clear();
|
|
||||||
_retryManager.clearAll();
|
_retryManager.clearAll();
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
unawaited(
|
||||||
|
_storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1372,13 +1754,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() {
|
||||||
_cancelAllTimers();
|
_cancelAllTimers();
|
||||||
|
_resetInMemoryState();
|
||||||
|
_retryManager.clearAll();
|
||||||
|
_persistMessages();
|
||||||
|
unawaited(
|
||||||
|
_storageService.saveRemovedSarMarkerIds(
|
||||||
|
_removedSarMarkerIds,
|
||||||
|
namespace: _storageNamespace,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _resetInMemoryState() {
|
||||||
_messages.clear();
|
_messages.clear();
|
||||||
_sarMarkers.clear();
|
_sarMarkers.clear();
|
||||||
_removedSarMarkerIds.clear();
|
_removedSarMarkerIds.clear();
|
||||||
@@ -1393,10 +1791,6 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_messageAckHistory.clear();
|
_messageAckHistory.clear();
|
||||||
_ackHistoryLookup.clear();
|
_ackHistoryLookup.clear();
|
||||||
_completedAckHistory.clear();
|
_completedAckHistory.clear();
|
||||||
_retryManager.clearAll();
|
|
||||||
_persistMessages();
|
|
||||||
unawaited(_storageService.saveRemovedSarMarkerIds(_removedSarMarkerIds));
|
|
||||||
notifyListeners();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int transferCountForSession({
|
int transferCountForSession({
|
||||||
@@ -1555,12 +1949,17 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
enhancedMessage = _resolveSenderNameIfNeeded(enhancedMessage);
|
||||||
|
final senderLocationSnapshot = _buildSentMessageLocationSnapshot();
|
||||||
|
|
||||||
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
// Check for duplicates (shouldn't happen for sent messages, but be safe)
|
||||||
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
if (_findDuplicateMessageIndex(enhancedMessage) != -1) {
|
||||||
debugPrint(
|
debugPrint(
|
||||||
'⚠️ [MessagesProvider] Duplicate sent message detected, skipping: ${enhancedMessage.id}',
|
'⚠️ [MessagesProvider] Duplicate sent message detected, skipping: ${enhancedMessage.id}',
|
||||||
);
|
);
|
||||||
|
if (senderLocationSnapshot != null) {
|
||||||
|
_messageContactLocations[enhancedMessage.id] = senderLocationSnapshot;
|
||||||
|
_persistMessages();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1570,6 +1969,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
isRead: true, // Sent messages are always marked as read
|
isRead: true, // Sent messages are always marked as read
|
||||||
);
|
);
|
||||||
_messages.add(sendingMessage);
|
_messages.add(sendingMessage);
|
||||||
|
if (senderLocationSnapshot != null) {
|
||||||
|
_messageContactLocations[sendingMessage.id] = senderLocationSnapshot;
|
||||||
|
}
|
||||||
debugPrint(' ✅ Message added to list at index ${_messages.length - 1}');
|
debugPrint(' ✅ Message added to list at index ${_messages.length - 1}');
|
||||||
debugPrint(' Total messages in list: ${_messages.length}');
|
debugPrint(' Total messages in list: ${_messages.length}');
|
||||||
|
|
||||||
@@ -1597,6 +1999,20 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
debugPrint(' ✅ notifyListeners() called - UI should update');
|
debugPrint(' ✅ notifyListeners() called - UI should update');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessageContactLocation? _buildSentMessageLocationSnapshot() {
|
||||||
|
final position = LocationTrackingService().currentPosition;
|
||||||
|
if (position == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return MessageContactLocation(
|
||||||
|
location: LatLng(position.latitude, position.longitude),
|
||||||
|
source: 'gps',
|
||||||
|
capturedAt: DateTime.now(),
|
||||||
|
sourceTimestamp: position.timestamp,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Register an individual message ID as part of a grouped message
|
/// Register an individual message ID as part of a grouped message
|
||||||
void registerGroupedMessageSend(
|
void registerGroupedMessageSend(
|
||||||
String individualMessageId,
|
String individualMessageId,
|
||||||
@@ -1828,7 +2244,9 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
int echoCount,
|
int echoCount,
|
||||||
int snrRaw,
|
int snrRaw,
|
||||||
int rssiDbm,
|
int rssiDbm,
|
||||||
) {
|
{
|
||||||
|
Uint8List? pathBytes,
|
||||||
|
}) {
|
||||||
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
|
debugPrint('🔊 [MessagesProvider] handleMessageEcho called');
|
||||||
debugPrint(' Message ID: $messageId');
|
debugPrint(' Message ID: $messageId');
|
||||||
debugPrint(' Echo count: $echoCount');
|
debugPrint(' Echo count: $echoCount');
|
||||||
@@ -1843,18 +2261,35 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
' ✅ Found message: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...',
|
' ✅ Found message: ${message.text.substring(0, message.text.length > 30 ? 30 : message.text.length)}...',
|
||||||
);
|
);
|
||||||
|
|
||||||
|
final nextEchoCount = echoCount > message.echoCount
|
||||||
|
? echoCount
|
||||||
|
: message.echoCount + 1;
|
||||||
|
|
||||||
// Update echo count
|
// Update echo count
|
||||||
final updatedMessage = message.copyWith(
|
final updatedMessage = message.copyWith(
|
||||||
echoCount: echoCount,
|
echoCount: nextEchoCount,
|
||||||
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
|
firstEchoAt: message.firstEchoAt ?? DateTime.now(),
|
||||||
lastEchoSnrRaw: snrRaw.toSigned(8),
|
lastEchoSnrRaw: snrRaw.toSigned(8),
|
||||||
lastEchoRssiDbm: rssiDbm.toSigned(8),
|
lastEchoRssiDbm: rssiDbm.toSigned(8),
|
||||||
lastEchoAt: DateTime.now(),
|
lastEchoAt: DateTime.now(),
|
||||||
);
|
);
|
||||||
_messages[index] = updatedMessage;
|
_messages[index] = updatedMessage;
|
||||||
|
_messageReceptionDetails[messageId] = _messageReceptionDetails[messageId]
|
||||||
|
?.copyWith(
|
||||||
|
capturedAt: DateTime.now(),
|
||||||
|
rssiDbm: rssiDbm.toSigned(8),
|
||||||
|
snrDb: snrRaw.toSigned(8) / 4.0,
|
||||||
|
pathBytes: pathBytes?.toList(),
|
||||||
|
) ??
|
||||||
|
MessageReceptionDetails(
|
||||||
|
capturedAt: DateTime.now(),
|
||||||
|
rssiDbm: rssiDbm.toSigned(8),
|
||||||
|
snrDb: snrRaw.toSigned(8) / 4.0,
|
||||||
|
pathBytes: pathBytes?.toList(),
|
||||||
|
);
|
||||||
_clearChannelSendWarning(messageId);
|
_clearChannelSendWarning(messageId);
|
||||||
|
|
||||||
debugPrint(' Updated echo count to: $echoCount');
|
debugPrint(' Updated echo count to: $nextEchoCount');
|
||||||
_persistMessages();
|
_persistMessages();
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
debugPrint(' ✅ Echo update complete, UI notified');
|
debugPrint(' ✅ Echo update complete, UI notified');
|
||||||
@@ -2256,7 +2691,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');
|
||||||
|
|
||||||
@@ -2383,6 +2818,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_pendingSentMessages.remove(message.expectedAckTag);
|
_pendingSentMessages.remove(message.expectedAckTag);
|
||||||
}
|
}
|
||||||
_clearAckHistoryForMessage(messageId);
|
_clearAckHistoryForMessage(messageId);
|
||||||
|
_removePendingDeliveredRouteRefresh(messageId);
|
||||||
|
|
||||||
// Clear retry tracking
|
// Clear retry tracking
|
||||||
_retryManager.clearRetry(messageId);
|
_retryManager.clearRetry(messageId);
|
||||||
@@ -2423,6 +2859,7 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
_pendingSentMessages.remove(message.expectedAckTag);
|
_pendingSentMessages.remove(message.expectedAckTag);
|
||||||
}
|
}
|
||||||
_clearAckHistoryForMessage(messageId);
|
_clearAckHistoryForMessage(messageId);
|
||||||
|
_removePendingDeliveredRouteRefresh(messageId);
|
||||||
_retryManager.clearRetry(messageId);
|
_retryManager.clearRetry(messageId);
|
||||||
_messageRouteMetadata.remove(messageId);
|
_messageRouteMetadata.remove(messageId);
|
||||||
onManualRetryPreparedCallback?.call(messageId);
|
onManualRetryPreparedCallback?.call(messageId);
|
||||||
@@ -2439,6 +2876,8 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
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,
|
||||||
@@ -2593,4 +3032,51 @@ class MessagesProvider with ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _prunePendingDeliveredRouteRefresh() {
|
||||||
|
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final cutoff = DateTime.now().subtract(_deliveredRouteRefreshWindow);
|
||||||
|
final emptyKeys = <String>[];
|
||||||
|
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
|
||||||
|
entry.value.removeWhere(
|
||||||
|
(pending) => pending.queuedAt.isBefore(cutoff),
|
||||||
|
);
|
||||||
|
if (entry.value.isEmpty) {
|
||||||
|
emptyKeys.add(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final key in emptyKeys) {
|
||||||
|
_pendingDeliveredRouteRefreshByContact.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removePendingDeliveredRouteRefresh(String messageId) {
|
||||||
|
if (_pendingDeliveredRouteRefreshByContact.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final emptyKeys = <String>[];
|
||||||
|
for (final entry in _pendingDeliveredRouteRefreshByContact.entries) {
|
||||||
|
entry.value.removeWhere((pending) => pending.messageId == messageId);
|
||||||
|
if (entry.value.isEmpty) {
|
||||||
|
emptyKeys.add(entry.key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final key in emptyKeys) {
|
||||||
|
_pendingDeliveredRouteRefreshByContact.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _PendingDeliveredRouteRefresh {
|
||||||
|
final String messageId;
|
||||||
|
final DateTime queuedAt;
|
||||||
|
|
||||||
|
const _PendingDeliveredRouteRefresh({
|
||||||
|
required this.messageId,
|
||||||
|
required this.queuedAt,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
582
lib/providers/offline_tiles_provider.dart
Normal file
@@ -0,0 +1,582 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
|
|
||||||
|
import '../models/map_layer.dart';
|
||||||
|
import '../services/offline_tile_cache_service.dart';
|
||||||
|
import '../services/tile_download_service.dart';
|
||||||
|
import '../services/tile_math_service.dart';
|
||||||
|
import '../services/tile_sharing_service.dart';
|
||||||
|
|
||||||
|
export '../services/tile_sharing_service.dart' show TilePeer, PeerCatalog;
|
||||||
|
export '../services/offline_tile_cache_service.dart'
|
||||||
|
show StyleInfo, DownloadRegion;
|
||||||
|
|
||||||
|
/// Download progress state.
|
||||||
|
class DownloadProgress {
|
||||||
|
final int downloaded;
|
||||||
|
final int skipped;
|
||||||
|
final int failed;
|
||||||
|
final int total;
|
||||||
|
|
||||||
|
const DownloadProgress({
|
||||||
|
this.downloaded = 0,
|
||||||
|
this.skipped = 0,
|
||||||
|
this.failed = 0,
|
||||||
|
this.total = 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
int get processed => downloaded + skipped + failed;
|
||||||
|
double get percent => total == 0 ? 0 : processed / total;
|
||||||
|
bool get isComplete => total > 0 && processed >= total;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A downloaded/skipped tile rectangle for map overlay.
|
||||||
|
class TileOverlay {
|
||||||
|
final double north, south, east, west;
|
||||||
|
final bool isSkipped;
|
||||||
|
|
||||||
|
const TileOverlay({
|
||||||
|
required this.north,
|
||||||
|
required this.south,
|
||||||
|
required this.east,
|
||||||
|
required this.west,
|
||||||
|
this.isSkipped = false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drawing mode for polygon selection.
|
||||||
|
enum DrawingMode { none, polygon, rectangle }
|
||||||
|
|
||||||
|
/// State management for offline tile downloading.
|
||||||
|
class OfflineTilesProvider extends ChangeNotifier {
|
||||||
|
final OfflineTileCacheService _cache = OfflineTileCacheService.instance;
|
||||||
|
final TileSharingService _sharing = TileSharingService.instance;
|
||||||
|
TileDownloadService? _downloadService;
|
||||||
|
StreamSubscription<TileDownloadEvent>? _downloadSubscription;
|
||||||
|
StreamSubscription<Set<TilePeer>>? _peersSubscription;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
|
// Drawing state
|
||||||
|
DrawingMode _drawingMode = DrawingMode.none;
|
||||||
|
DrawingMode _downloadSelectionMode = DrawingMode.none;
|
||||||
|
final List<List<LatLng>> _polygons = [];
|
||||||
|
List<LatLng> _currentVertices = [];
|
||||||
|
LatLng? _rectangleFirstCorner;
|
||||||
|
|
||||||
|
// Download settings
|
||||||
|
int _minZoom = 8;
|
||||||
|
int _maxZoom = 14;
|
||||||
|
MapLayer _selectedLayer = MapLayer.openStreetMap;
|
||||||
|
|
||||||
|
// Download progress
|
||||||
|
bool _isDownloading = false;
|
||||||
|
DownloadProgress _progress = const DownloadProgress();
|
||||||
|
final List<TileOverlay> _tileOverlays = [];
|
||||||
|
|
||||||
|
// Cache info
|
||||||
|
int _cacheSizeBytes = 0;
|
||||||
|
|
||||||
|
// Sharing state
|
||||||
|
bool _isServerRunning = false;
|
||||||
|
Set<TilePeer> _discoveredPeers = {};
|
||||||
|
List<PeerCatalog> _peerCatalogs = [];
|
||||||
|
bool _isFetchingCatalogs = false;
|
||||||
|
bool _isSyncing = false;
|
||||||
|
String _syncStatus = '';
|
||||||
|
double _syncProgress = 0;
|
||||||
|
|
||||||
|
// Local style info
|
||||||
|
List<StyleInfo> _localStyles = [];
|
||||||
|
|
||||||
|
// Coverage overlay — which cached style's tiles to show on the map
|
||||||
|
StyleInfo? _coverageStyle;
|
||||||
|
List<TileOverlay> _coverageOverlays = [];
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
DrawingMode get drawingMode => _drawingMode;
|
||||||
|
DrawingMode get downloadSelectionMode => _downloadSelectionMode;
|
||||||
|
List<List<LatLng>> get polygons => List.unmodifiable(_polygons);
|
||||||
|
List<LatLng> get currentVertices => List.unmodifiable(_currentVertices);
|
||||||
|
LatLng? get rectangleFirstCorner => _rectangleFirstCorner;
|
||||||
|
int get minZoom => _minZoom;
|
||||||
|
int get maxZoom => _maxZoom;
|
||||||
|
MapLayer get selectedLayer => _selectedLayer;
|
||||||
|
bool get isDownloading => _isDownloading;
|
||||||
|
DownloadProgress get progress => _progress;
|
||||||
|
List<TileOverlay> get tileOverlays => _tileOverlays;
|
||||||
|
int get cacheSizeBytes => _cacheSizeBytes;
|
||||||
|
bool get hasPolygons => _polygons.isNotEmpty;
|
||||||
|
bool get isServerRunning => _isServerRunning;
|
||||||
|
Set<TilePeer> get discoveredPeers => _discoveredPeers;
|
||||||
|
List<PeerCatalog> get peerCatalogs => _peerCatalogs;
|
||||||
|
bool get isFetchingCatalogs => _isFetchingCatalogs;
|
||||||
|
bool get isSyncing => _isSyncing;
|
||||||
|
String get syncStatus => _syncStatus;
|
||||||
|
double get syncProgress => _syncProgress;
|
||||||
|
List<StyleInfo> get localStyles => _localStyles;
|
||||||
|
StyleInfo? get coverageStyle => _coverageStyle;
|
||||||
|
List<TileOverlay> get coverageOverlays => _coverageOverlays;
|
||||||
|
|
||||||
|
/// Estimated tile count for the current selection.
|
||||||
|
int get estimatedTileCount {
|
||||||
|
if (_polygons.isEmpty) return 0;
|
||||||
|
return TileMathService.estimateTileCount(_polygons, _minZoom, _maxZoom);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drawing methods
|
||||||
|
|
||||||
|
void setDrawingMode(DrawingMode mode) {
|
||||||
|
_drawingMode = mode;
|
||||||
|
_currentVertices = [];
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void startDownloadSelectionMode(DrawingMode mode) {
|
||||||
|
_polygons.clear();
|
||||||
|
_currentVertices = [];
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
_downloadSelectionMode = mode;
|
||||||
|
_drawingMode = mode;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addVertex(LatLng point) {
|
||||||
|
if (_drawingMode == DrawingMode.polygon) {
|
||||||
|
_currentVertices = [..._currentVertices, point];
|
||||||
|
notifyListeners();
|
||||||
|
} else if (_drawingMode == DrawingMode.rectangle) {
|
||||||
|
if (_rectangleFirstCorner == null) {
|
||||||
|
_rectangleFirstCorner = point;
|
||||||
|
notifyListeners();
|
||||||
|
} else {
|
||||||
|
// Complete rectangle
|
||||||
|
final corner1 = _rectangleFirstCorner!;
|
||||||
|
final corner2 = point;
|
||||||
|
final rect = [
|
||||||
|
LatLng(corner1.latitude, corner1.longitude),
|
||||||
|
LatLng(corner1.latitude, corner2.longitude),
|
||||||
|
LatLng(corner2.latitude, corner2.longitude),
|
||||||
|
LatLng(corner2.latitude, corner1.longitude),
|
||||||
|
];
|
||||||
|
_polygons.add(rect);
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
_drawingMode = DrawingMode.none;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void finishPolygon() {
|
||||||
|
if (_drawingMode == DrawingMode.polygon && _currentVertices.length >= 3) {
|
||||||
|
_polygons.add(List.from(_currentVertices));
|
||||||
|
_currentVertices = [];
|
||||||
|
_drawingMode = DrawingMode.none;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePolygon(int index) {
|
||||||
|
if (index >= 0 && index < _polygons.length) {
|
||||||
|
_polygons.removeAt(index);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearPolygons() {
|
||||||
|
_polygons.clear();
|
||||||
|
_currentVertices = [];
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
_drawingMode = DrawingMode.none;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setCurrentViewBounds({
|
||||||
|
required double north,
|
||||||
|
required double south,
|
||||||
|
required double east,
|
||||||
|
required double west,
|
||||||
|
}) {
|
||||||
|
_polygons
|
||||||
|
..clear()
|
||||||
|
..add([
|
||||||
|
LatLng(north, west),
|
||||||
|
LatLng(north, east),
|
||||||
|
LatLng(south, east),
|
||||||
|
LatLng(south, west),
|
||||||
|
]);
|
||||||
|
_currentVertices = [];
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
_downloadSelectionMode = DrawingMode.none;
|
||||||
|
_drawingMode = DrawingMode.none;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void undoLastVertex() {
|
||||||
|
if (_currentVertices.isNotEmpty) {
|
||||||
|
_currentVertices = _currentVertices.sublist(0, _currentVertices.length - 1);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download settings
|
||||||
|
|
||||||
|
void setMinZoom(int zoom) {
|
||||||
|
_minZoom = zoom.clamp(0, 19);
|
||||||
|
if (_maxZoom < _minZoom) _maxZoom = _minZoom;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setMaxZoom(int zoom) {
|
||||||
|
_maxZoom = zoom.clamp(0, 19);
|
||||||
|
if (_minZoom > _maxZoom) _minZoom = _maxZoom;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedLayer(MapLayer layer) {
|
||||||
|
_selectedLayer = layer;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setSelectedLayerIfDifferent(MapLayer layer) {
|
||||||
|
if (_selectedLayer.type == layer.type &&
|
||||||
|
_selectedLayer.urlTemplate == layer.urlTemplate) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_selectedLayer = layer;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download control
|
||||||
|
|
||||||
|
Future<void> startDownload() async {
|
||||||
|
if (_isDownloading || _polygons.isEmpty) return;
|
||||||
|
|
||||||
|
_isDownloading = true;
|
||||||
|
_progress = const DownloadProgress();
|
||||||
|
_tileOverlays.clear();
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
_downloadService = TileDownloadService();
|
||||||
|
final stream = _downloadService!.downloadTiles(
|
||||||
|
polygons: _polygons,
|
||||||
|
minZoom: _minZoom,
|
||||||
|
maxZoom: _maxZoom,
|
||||||
|
urlTemplate: _selectedLayer.urlTemplate,
|
||||||
|
displayName: _selectedLayer.name,
|
||||||
|
headers: _selectedLayer.headers,
|
||||||
|
);
|
||||||
|
|
||||||
|
await for (final event in stream) {
|
||||||
|
switch (event) {
|
||||||
|
case TileDownloadStarted(:final totalTiles):
|
||||||
|
_progress = DownloadProgress(total: totalTiles);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileDownloaded(:final north, :final south, :final east, :final west):
|
||||||
|
_progress = DownloadProgress(
|
||||||
|
downloaded: _progress.downloaded + 1,
|
||||||
|
skipped: _progress.skipped,
|
||||||
|
failed: _progress.failed,
|
||||||
|
total: _progress.total,
|
||||||
|
);
|
||||||
|
_addOverlay(TileOverlay(
|
||||||
|
north: north, south: south, east: east, west: west,
|
||||||
|
));
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileSkipped():
|
||||||
|
_progress = DownloadProgress(
|
||||||
|
downloaded: _progress.downloaded,
|
||||||
|
skipped: _progress.skipped + 1,
|
||||||
|
failed: _progress.failed,
|
||||||
|
total: _progress.total,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileBatchSkipped(:final count):
|
||||||
|
_progress = DownloadProgress(
|
||||||
|
downloaded: _progress.downloaded,
|
||||||
|
skipped: _progress.skipped + count,
|
||||||
|
failed: _progress.failed,
|
||||||
|
total: _progress.total,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileFailed():
|
||||||
|
_progress = DownloadProgress(
|
||||||
|
downloaded: _progress.downloaded,
|
||||||
|
skipped: _progress.skipped,
|
||||||
|
failed: _progress.failed + 1,
|
||||||
|
total: _progress.total,
|
||||||
|
);
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileDownloadComplete():
|
||||||
|
_isDownloading = false;
|
||||||
|
_tileOverlays.clear();
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case TileDownloadCancelled():
|
||||||
|
_isDownloading = false;
|
||||||
|
_tileOverlays.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_isDownloading = false;
|
||||||
|
_downloadService?.dispose();
|
||||||
|
_downloadService = null;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelDownload() {
|
||||||
|
_downloadService?.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearOverlays() {
|
||||||
|
_tileOverlays.clear();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addOverlay(TileOverlay overlay) {
|
||||||
|
_tileOverlays.add(overlay);
|
||||||
|
// Limit overlays to prevent OOM
|
||||||
|
if (_tileOverlays.length > 500) {
|
||||||
|
_tileOverlays.removeRange(0, _tileOverlays.length - 500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache management
|
||||||
|
|
||||||
|
Future<void> refreshCacheSize() async {
|
||||||
|
_cacheSizeBytes = await _cache.getCacheSize();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> deleteStyle(StyleInfo style) async {
|
||||||
|
if (_coverageStyle?.hash == style.hash) hideCoverage();
|
||||||
|
await _cache.deleteStyle(style.hash);
|
||||||
|
await refreshCacheSize();
|
||||||
|
await refreshLocalStyles();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> clearCache() async {
|
||||||
|
hideCoverage();
|
||||||
|
await _cache.clearCache();
|
||||||
|
_cacheSizeBytes = 0;
|
||||||
|
_localStyles = [];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coverage overlay — show cached tile bounds on the map
|
||||||
|
|
||||||
|
/// Show the coverage of a cached style on the map.
|
||||||
|
/// Loads tile coordinates from the manifest and converts to bounds.
|
||||||
|
Future<void> showCoverage(StyleInfo style) async {
|
||||||
|
if (_coverageStyle?.hash == style.hash) {
|
||||||
|
// Toggle off if same style tapped again
|
||||||
|
hideCoverage();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_coverageStyle = style;
|
||||||
|
_coverageOverlays = [];
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final tiles = await _cache.listTilesForStyle(style.hash);
|
||||||
|
|
||||||
|
// Convert cached tile coordinates to bound overlays
|
||||||
|
final overlays = <TileOverlay>[];
|
||||||
|
for (final tile in tiles) {
|
||||||
|
final bounds = TileMathService.tileBounds(tile.x, tile.y, tile.z);
|
||||||
|
overlays.add(TileOverlay(
|
||||||
|
north: bounds.north,
|
||||||
|
south: bounds.south,
|
||||||
|
east: bounds.east,
|
||||||
|
west: bounds.west,
|
||||||
|
isSkipped: true, // green color
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
_coverageOverlays = overlays;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void hideCoverage() {
|
||||||
|
_coverageStyle = null;
|
||||||
|
_coverageOverlays = [];
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sharing controls
|
||||||
|
|
||||||
|
Future<void> toggleServer() async {
|
||||||
|
if (_isServerRunning) {
|
||||||
|
await _sharing.stopServer();
|
||||||
|
_isServerRunning = false;
|
||||||
|
} else {
|
||||||
|
await _sharing.startServer();
|
||||||
|
_isServerRunning = _sharing.isRunning;
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> startPeerDiscovery() async {
|
||||||
|
_peersSubscription?.cancel();
|
||||||
|
_peersSubscription = _sharing.peersStream.listen((peers) {
|
||||||
|
_discoveredPeers = peers;
|
||||||
|
notifyListeners();
|
||||||
|
});
|
||||||
|
await _sharing.startDiscovery();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> stopPeerDiscovery() async {
|
||||||
|
_peersSubscription?.cancel();
|
||||||
|
_peersSubscription = null;
|
||||||
|
await _sharing.stopPeerDiscovery();
|
||||||
|
_discoveredPeers = {};
|
||||||
|
if (!_isDisposed) {
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void addManualPeer(String ipAddress) {
|
||||||
|
_sharing.addManualPeer(ipAddress);
|
||||||
|
_discoveredPeers = _sharing.discoveredPeers;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
void removePeer(TilePeer peer) {
|
||||||
|
_sharing.removePeer(peer);
|
||||||
|
_discoveredPeers = _sharing.discoveredPeers;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Peer catalog & P2P sync
|
||||||
|
|
||||||
|
/// Refresh local style info.
|
||||||
|
Future<void> refreshLocalStyles() async {
|
||||||
|
_localStyles = await _cache.listStylesDetailed();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch catalogs from all discovered peers to see what they have.
|
||||||
|
Future<void> refreshPeerCatalogs() async {
|
||||||
|
_isFetchingCatalogs = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
_peerCatalogs = await _sharing.fetchAllPeerCatalogs();
|
||||||
|
_isFetchingCatalogs = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sync a style from one or more peers that have it.
|
||||||
|
/// Finds all peers offering [styleHash] and pulls missing tiles.
|
||||||
|
Future<void> syncStyleFromPeers(StyleInfo style) async {
|
||||||
|
if (_isSyncing) return;
|
||||||
|
|
||||||
|
// Find all peers that have this style
|
||||||
|
final peersWithStyle = <TilePeer>[];
|
||||||
|
for (final catalog in _peerCatalogs) {
|
||||||
|
if (catalog.styles.any((s) => s.hash == style.hash)) {
|
||||||
|
peersWithStyle.add(catalog.peer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (peersWithStyle.isEmpty) return;
|
||||||
|
|
||||||
|
_isSyncing = true;
|
||||||
|
_syncStatus = 'Starting sync of ${style.displayName}...';
|
||||||
|
_syncProgress = 0;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final stream = _sharing.syncStyleFromPeers(
|
||||||
|
peers: peersWithStyle,
|
||||||
|
styleHash: style.hash,
|
||||||
|
styleMeta: style,
|
||||||
|
);
|
||||||
|
|
||||||
|
await for (final event in stream) {
|
||||||
|
switch (event) {
|
||||||
|
case PeerSyncStarted(:final totalTiles):
|
||||||
|
_syncStatus = 'Syncing ${style.displayName}: 0/$totalTiles tiles';
|
||||||
|
_syncProgress = 0;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case PeerSyncTileDownloaded(:final downloaded, :final total):
|
||||||
|
_syncStatus =
|
||||||
|
'Syncing ${style.displayName}: $downloaded/$total tiles';
|
||||||
|
_syncProgress = total > 0 ? downloaded / total : 0;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case PeerSyncTileSkipped(:final skipped, :final total):
|
||||||
|
_syncProgress = total > 0 ? skipped / total : 0;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
case PeerSyncComplete(:final downloaded, :final skipped, :final failed):
|
||||||
|
_syncStatus =
|
||||||
|
'Done! $downloaded new, $skipped cached, $failed failed';
|
||||||
|
_isSyncing = false;
|
||||||
|
notifyListeners();
|
||||||
|
await refreshCacheSize();
|
||||||
|
await refreshLocalStyles();
|
||||||
|
|
||||||
|
case PeerSyncCancelled():
|
||||||
|
_syncStatus = 'Sync cancelled';
|
||||||
|
_isSyncing = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void cancelSync() {
|
||||||
|
_sharing.cancelSync();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Presets — load a previously downloaded region for quick re-download
|
||||||
|
|
||||||
|
/// Load a saved download region as the current selection.
|
||||||
|
/// Restores polygons, zoom range, and map layer.
|
||||||
|
void loadPreset(StyleInfo style) {
|
||||||
|
if (style.region == null) return;
|
||||||
|
|
||||||
|
final region = style.region!;
|
||||||
|
|
||||||
|
// Restore polygons
|
||||||
|
_polygons.clear();
|
||||||
|
for (final polyData in region.polygons) {
|
||||||
|
final poly = polyData.map((v) => LatLng(v[0], v[1])).toList();
|
||||||
|
if (poly.length >= 3) _polygons.add(poly);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore zoom range
|
||||||
|
_minZoom = region.minZoom;
|
||||||
|
_maxZoom = region.maxZoom;
|
||||||
|
|
||||||
|
// Try to find the matching map layer
|
||||||
|
if (style.urlTemplate.isNotEmpty) {
|
||||||
|
final matchingLayer = MapLayer.allLayers.where(
|
||||||
|
(l) => l.urlTemplate == style.urlTemplate,
|
||||||
|
);
|
||||||
|
if (matchingLayer.isNotEmpty) {
|
||||||
|
_selectedLayer = matchingLayer.first;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_currentVertices = [];
|
||||||
|
_rectangleFirstCorner = null;
|
||||||
|
_drawingMode = DrawingMode.none;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
|
_downloadSubscription?.cancel();
|
||||||
|
_downloadService?.dispose();
|
||||||
|
_peersSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,16 +5,60 @@ 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';
|
||||||
|
|
||||||
enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
enum SensorRefreshState { idle, refreshing, success, timeout, unavailable }
|
||||||
|
|
||||||
|
class SensorHistorySample {
|
||||||
|
final DateTime timestamp;
|
||||||
|
final Map<String, double> values;
|
||||||
|
|
||||||
|
const SensorHistorySample({required this.timestamp, required this.values});
|
||||||
|
|
||||||
|
factory SensorHistorySample.fromJson(Map<String, dynamic> json) {
|
||||||
|
final rawValues = json['values'] as Map<String, dynamic>? ?? const {};
|
||||||
|
return SensorHistorySample(
|
||||||
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
|
(json['timestampMillis'] as num?)?.toInt() ?? 0,
|
||||||
|
),
|
||||||
|
values: rawValues.map(
|
||||||
|
(key, value) => MapEntry(key, (value as num).toDouble()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => {
|
||||||
|
'timestampMillis': timestamp.millisecondsSinceEpoch,
|
||||||
|
'values': values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class SensorsProvider with ChangeNotifier {
|
class SensorsProvider with ChangeNotifier {
|
||||||
static const Duration _successStateRetention = Duration(minutes: 1);
|
static const Duration _successStateRetention = Duration(minutes: 1);
|
||||||
|
static const Duration selfAutoRefreshInterval = Duration(seconds: 30);
|
||||||
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 String _historyKey = 'sensor_history_v1';
|
||||||
|
static const String _telemetrySourceChannelPrefix = '__source_channel:';
|
||||||
|
static const String _rawTelemetryHexKey = '__raw_lpp_hex';
|
||||||
|
static const int _maxHistorySamplesPerSensor = 288;
|
||||||
|
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 +67,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 +84,24 @@ 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>{};
|
||||||
|
final Map<String, List<SensorHistorySample>> _historyBySensor =
|
||||||
|
<String, List<SensorHistorySample>>{};
|
||||||
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 +112,28 @@ 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),
|
||||||
|
);
|
||||||
|
final storedHistoryJson = prefs.getString(_key(_historyKey));
|
||||||
_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();
|
||||||
|
_historyBySensor.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 +149,64 @@ 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (storedHistoryJson != null && storedHistoryJson.isNotEmpty) {
|
||||||
|
final decoded = jsonDecode(storedHistoryJson) as Map<String, dynamic>;
|
||||||
|
for (final entry in decoded.entries) {
|
||||||
|
final rawSamples = entry.value as List<dynamic>? ?? const [];
|
||||||
|
final samples = rawSamples
|
||||||
|
.whereType<Map<String, dynamic>>()
|
||||||
|
.map(SensorHistorySample.fromJson)
|
||||||
|
.where((sample) => sample.values.isNotEmpty)
|
||||||
|
.toList()
|
||||||
|
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
if (samples.isNotEmpty) {
|
||||||
|
_historyBySensor[entry.key] = samples;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_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 +216,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 +237,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 +249,89 @@ 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _persistHistory() async {
|
||||||
|
try {
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
final encoded = <String, dynamic>{};
|
||||||
|
for (final entry in _historyBySensor.entries) {
|
||||||
|
if (entry.value.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
encoded[entry.key] = entry.value
|
||||||
|
.map((sample) => sample.toJson())
|
||||||
|
.toList(growable: false);
|
||||||
|
}
|
||||||
|
await prefs.setString(_key(_historyKey), jsonEncode(encoded));
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('Error saving sensor history: $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 +341,123 @@ 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;
|
||||||
|
|
||||||
|
List<SensorHistorySample> historyFor(String publicKeyHex) =>
|
||||||
|
List<SensorHistorySample>.unmodifiable(
|
||||||
|
_historyBySensor[publicKeyHex] ?? const <SensorHistorySample>[],
|
||||||
|
);
|
||||||
|
|
||||||
|
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 _isRefreshDueForInterval(
|
||||||
|
String publicKeyHex, {
|
||||||
|
required Duration interval,
|
||||||
|
required DateTime refreshTime,
|
||||||
|
}) {
|
||||||
|
if (interval <= Duration.zero) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final lastRefreshAt = _lastRefreshAttemptAt[publicKeyHex];
|
||||||
|
return lastRefreshAt == null ||
|
||||||
|
refreshTime.difference(lastRefreshAt) >= interval;
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> toggleMetric(
|
Future<void> toggleMetric(
|
||||||
String publicKeyHex,
|
String publicKeyHex,
|
||||||
String fieldKey,
|
String fieldKey,
|
||||||
@@ -140,8 +467,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 +485,9 @@ class SensorsProvider with ChangeNotifier {
|
|||||||
visibleFields.remove(fieldKey);
|
visibleFields.remove(fieldKey);
|
||||||
}
|
}
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
|
if (shouldPersistOrder) {
|
||||||
|
await _persistMetricOrder();
|
||||||
|
}
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,11 +505,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 +590,185 @@ 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);
|
||||||
|
_historyBySensor.remove(publicKeyHex);
|
||||||
await _persistWatchedSensors();
|
await _persistWatchedSensors();
|
||||||
await _persistVisibleMetrics();
|
await _persistVisibleMetrics();
|
||||||
await _persistFieldSpans();
|
await _persistFieldSpans();
|
||||||
|
await _persistMetricLabels();
|
||||||
|
await _persistMetricOrder();
|
||||||
|
await _persistAutoRefreshMinutes();
|
||||||
|
await _persistHistory();
|
||||||
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 +776,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 +793,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 +825,84 @@ 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) &&
|
||||||
|
_isRefreshDueForInterval(
|
||||||
|
self.publicKeyHex,
|
||||||
|
interval: selfAutoRefreshInterval,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> captureTrackedTelemetryHistory({
|
||||||
|
required ContactsProvider contactsProvider,
|
||||||
|
required ConnectionProvider connectionProvider,
|
||||||
|
}) async {
|
||||||
|
final trackedKeys = <String>{
|
||||||
|
..._watchedSensorKeys.where((key) => autoRefreshMinutesFor(key) > 0),
|
||||||
|
};
|
||||||
|
final self = selfContact(contactsProvider, connectionProvider);
|
||||||
|
if (self != null && _lastRefreshAttemptAt.containsKey(self.publicKeyHex)) {
|
||||||
|
trackedKeys.add(self.publicKeyHex);
|
||||||
|
}
|
||||||
|
if (trackedKeys.isEmpty) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var changed = false;
|
||||||
|
for (final key in trackedKeys) {
|
||||||
|
final contact = contactForDisplay(
|
||||||
|
key,
|
||||||
|
contactsProvider: contactsProvider,
|
||||||
|
connectionProvider: connectionProvider,
|
||||||
|
);
|
||||||
|
if (contact == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
changed = _captureTelemetryHistory(contact) || changed;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await _persistHistory();
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
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 +934,125 @@ 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _captureTelemetryHistory(Contact contact) {
|
||||||
|
final telemetry = contact.telemetry;
|
||||||
|
if (telemetry == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final values = _historyValuesForTelemetry(telemetry);
|
||||||
|
if (values.isEmpty) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final samples = _historyBySensor.putIfAbsent(
|
||||||
|
contact.publicKeyHex,
|
||||||
|
() => <SensorHistorySample>[],
|
||||||
|
);
|
||||||
|
final timestamp = telemetry.timestamp;
|
||||||
|
final existingIndex = samples.lastIndexWhere(
|
||||||
|
(sample) => sample.timestamp.millisecondsSinceEpoch ==
|
||||||
|
timestamp.millisecondsSinceEpoch,
|
||||||
|
);
|
||||||
|
final nextSample = SensorHistorySample(timestamp: timestamp, values: values);
|
||||||
|
|
||||||
|
if (existingIndex >= 0) {
|
||||||
|
final current = samples[existingIndex];
|
||||||
|
if (mapEquals(current.values, values)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
samples[existingIndex] = nextSample;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
samples.add(nextSample);
|
||||||
|
samples.sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
if (samples.length > _maxHistorySamplesPerSensor) {
|
||||||
|
samples.removeRange(0, samples.length - _maxHistorySamplesPerSensor);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, double> _historyValuesForTelemetry(ContactTelemetry telemetry) {
|
||||||
|
final values = <String, double>{};
|
||||||
|
|
||||||
|
if (telemetry.batteryMilliVolts != null) {
|
||||||
|
values['voltage'] = telemetry.batteryMilliVolts! / 1000;
|
||||||
|
}
|
||||||
|
if (telemetry.batteryPercentage != null) {
|
||||||
|
values['battery'] = telemetry.batteryPercentage!;
|
||||||
|
}
|
||||||
|
if (telemetry.temperature != null) {
|
||||||
|
values['temperature'] = telemetry.temperature!;
|
||||||
|
}
|
||||||
|
if (telemetry.humidity != null) {
|
||||||
|
values['humidity'] = telemetry.humidity!;
|
||||||
|
}
|
||||||
|
if (telemetry.pressure != null) {
|
||||||
|
values['pressure'] = telemetry.pressure!;
|
||||||
|
}
|
||||||
|
|
||||||
|
final extraSensorData = telemetry.extraSensorData;
|
||||||
|
if (extraSensorData != null) {
|
||||||
|
for (final entry in extraSensorData.entries) {
|
||||||
|
if (_isTelemetryMetadataKey(entry.key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final numericValue = _historyNumericValue(entry.value);
|
||||||
|
if (numericValue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
values[_extraFieldKey(entry.key)] = numericValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
double? _historyNumericValue(dynamic value) {
|
||||||
|
if (value is num) {
|
||||||
|
return value.toDouble();
|
||||||
|
}
|
||||||
|
if (value is bool) {
|
||||||
|
return value ? 1 : 0;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isTelemetryMetadataKey(String key) {
|
||||||
|
return key.startsWith(_telemetrySourceChannelPrefix) ||
|
||||||
|
key == _rawTelemetryHexKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _extraFieldKey(String key) {
|
||||||
|
return 'extra:$key';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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: AppLocalizations.of(context)!.pasteShareLinkOrHexAdvert,
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||